diff --git a/.github/workflows/mcp-server-ci.yml b/.github/workflows/mcp-server-ci.yml new file mode 100644 index 0000000..e13a556 --- /dev/null +++ b/.github/workflows/mcp-server-ci.yml @@ -0,0 +1,209 @@ +name: MCP Server CI/CD + +on: + push: + branches: + - main + - master + - develop + paths: + - 'packages/mcp-server/**' + - '.github/workflows/mcp-server-ci.yml' + pull_request: + paths: + - 'packages/mcp-server/**' + - '.github/workflows/mcp-server-ci.yml' + release: + types: [published] + +env: + REGISTRY: docker.io + IMAGE_NAME: coorchat/mcp-server + NODE_VERSION: '18' + +jobs: + lint-and-test: + name: Lint & Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/mcp-server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: packages/mcp-server/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run type check + run: npx tsc --noEmit + + - name: Run unit tests + run: npm test -- --coverage + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + with: + files: ./packages/mcp-server/coverage/lcov.info + flags: mcp-server + name: mcp-server-coverage + fail_ci_if_error: false + + build: + name: Build TypeScript + runs-on: ubuntu-latest + needs: lint-and-test + defaults: + run: + working-directory: packages/mcp-server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: packages/mcp-server/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: mcp-server-dist + path: packages/mcp-server/dist + retention-days: 7 + + integration-test: + name: Integration Tests + runs-on: ubuntu-latest + needs: build + defaults: + run: + working-directory: packages/mcp-server + + services: + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: packages/mcp-server/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run integration tests + run: npm run test:integration + env: + REDIS_URL: redis://localhost:6379 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker-build: + name: Build Docker Image + runs-on: ubuntu-latest + needs: [lint-and-test, build] + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./packages/mcp-server + file: ./packages/mcp-server/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + publish-npm: + name: Publish to npm + runs-on: ubuntu-latest + needs: [integration-test, docker-build] + if: github.event_name == 'release' + defaults: + run: + working-directory: packages/mcp-server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Publish to npm + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/relay-server-ci.yml b/.github/workflows/relay-server-ci.yml new file mode 100644 index 0000000..d6cd869 --- /dev/null +++ b/.github/workflows/relay-server-ci.yml @@ -0,0 +1,186 @@ +name: Relay Server CI/CD + +on: + push: + branches: + - main + - master + - develop + paths: + - 'packages/relay-server/**' + - '.github/workflows/relay-server-ci.yml' + pull_request: + paths: + - 'packages/relay-server/**' + - '.github/workflows/relay-server-ci.yml' + release: + types: [published] + +env: + REGISTRY: docker.io + IMAGE_NAME: coorchat/relay-server + DOTNET_VERSION: '8.0' + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/relay-server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore + + - name: Build solution + run: dotnet build --no-restore --configuration Release + + - name: Run unit tests + run: dotnet test tests/CoorChat.RelayServer.Tests.Unit/CoorChat.RelayServer.Tests.Unit.csproj --no-build --configuration Release --logger trx --collect:"XPlat Code Coverage" + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: packages/relay-server/**/*.trx + retention-days: 7 + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + with: + files: ./packages/relay-server/**/coverage.cobertura.xml + flags: relay-server + name: relay-server-coverage + fail_ci_if_error: false + + integration-test: + name: Integration Tests + runs-on: ubuntu-latest + needs: build-and-test + defaults: + run: + working-directory: packages/relay-server + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: coorchat + POSTGRES_PASSWORD: test_password + POSTGRES_DB: coorchat_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore + + - name: Build solution + run: dotnet build --no-restore --configuration Release + + - name: Run integration tests + run: dotnet test tests/CoorChat.RelayServer.Tests.Integration/CoorChat.RelayServer.Tests.Integration.csproj --no-build --configuration Release --logger trx + env: + ConnectionStrings__DefaultConnection: "Host=localhost;Port=5432;Database=coorchat_test;Username=coorchat;Password=test_password" + + docker-build: + name: Build Docker Image + runs-on: ubuntu-latest + needs: [build-and-test, integration-test] + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./packages/relay-server + file: ./packages/relay-server/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + publish-nuget: + name: Publish to NuGet + runs-on: ubuntu-latest + needs: [integration-test, docker-build] + if: github.event_name == 'release' + defaults: + run: + working-directory: packages/relay-server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore + + - name: Build solution + run: dotnet build --no-restore --configuration Release + + - name: Pack NuGet packages + run: | + dotnet pack src/CoorChat.RelayServer.Core/CoorChat.RelayServer.Core.csproj --no-build --configuration Release --output ./nupkgs + dotnet pack src/CoorChat.RelayServer.Data/CoorChat.RelayServer.Data.csproj --no-build --configuration Release --output ./nupkgs + + - name: Publish to NuGet + run: dotnet nuget push "./nupkgs/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad0a823 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Dependencies +node_modules/ +packages/*/node_modules/ + +# Build outputs +dist/ +build/ +out/ +*.js.map +*.d.ts.map + +# TypeScript +*.tsbuildinfo + +# .NET +bin/ +obj/ +*.user +*.suo +packages/relay-server/packages/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Environment variables +.env +.env.local +.env.*.local +*.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Testing +coverage/ +.nyc_output/ + +# Config +.coorchat/config.json +.coorchat/*.json +!.coorchat/config.template.yaml + +# Temporary files +*.tmp +*.temp +.cache/ + +# Docker +.dockerignore diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3f387de --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# coorchat Development Guidelines + +Auto-generated from all feature plans. Last updated: 2026-02-14 + +## Active Technologies + + + +## Project Structure + +```text +src/ +tests/ +``` + +## Commands + +# Add commands for + +## Code Style + +General: Follow standard conventions + +## Recent Changes + + + + + diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..4448808 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,355 @@ +# CoorChat Local Installation Guide + +## Prerequisites + +- **Node.js** 18+ and npm +- **Docker** and Docker Compose (optional, for Redis/Relay Server) +- **Git** (for cloning) +- **Claude Desktop** (for MCP integration) + +## Quick Start (5 minutes) + +### Step 1: Install Dependencies + +```bash +# Navigate to MCP server package +cd packages/mcp-server + +# Install dependencies +npm install + +# Build the project +npm run build +``` + +### Step 2: Generate Secure Token + +```bash +# Generate a secure shared token (on Windows/Git Bash) +node -e "console.log('cct_' + require('crypto').randomBytes(32).toString('hex'))" + +# Save the output - you'll need it for all agents +# Example output: cct_a3f8d9e2c1b4f7a6e8d2c9b1f4a7e3d2c6b9f1e4a8d3c7b2f5e9a1d4c8b3f6 +``` + +### Step 3: Choose Your Channel + +You have **3 options** - pick the easiest for you: + +#### **Option A: Redis (Recommended - Most Reliable)** + +```bash +# Start Redis using Docker +docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine + +# Or install Redis locally on Windows: +# Download from: https://github.com/microsoftarchive/redis/releases +``` + +#### **Option B: Discord (Easiest - No Setup)** + +1. Go to https://discord.com/developers/applications +2. Create a new application +3. Go to "Bot" β†’ "Add Bot" +4. Copy the bot token +5. Enable "Message Content Intent" +6. Invite bot to your server using OAuth2 URL generator +7. Create a channel and copy its ID (right-click β†’ Copy ID) + +#### **Option C: SignalR Relay Server (Advanced)** + +```bash +# From repo root +cd packages/relay-server + +# Build and run with Docker +docker build -t coorchat-relay . +docker run -d -p 5001:5001 -e Authentication__SharedToken=YOUR_TOKEN coorchat-relay +``` + +### Step 4: Configure MCP Server for Claude + +Create a config file at: `C:\Users\YourUser\.claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "coorchat": { + "command": "node", + "args": [ + "C:\\projects\\coorchat\\packages\\mcp-server\\dist\\index.js" + ], + "env": { + "CHANNEL_TYPE": "redis", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "SHARED_TOKEN": "cct_YOUR_TOKEN_FROM_STEP2", + "AGENT_ID": "agent-claude-1", + "AGENT_ROLE": "developer", + "LOG_LEVEL": "info" + } + } + } +} +``` + +**For Discord instead:** +```json +{ + "mcpServers": { + "coorchat": { + "command": "node", + "args": [ + "C:\\projects\\coorchat\\packages\\mcp-server\\dist\\index.js" + ], + "env": { + "CHANNEL_TYPE": "discord", + "DISCORD_BOT_TOKEN": "YOUR_BOT_TOKEN", + "DISCORD_CHANNEL_ID": "YOUR_CHANNEL_ID", + "SHARED_TOKEN": "cct_YOUR_TOKEN_FROM_STEP2", + "AGENT_ID": "agent-claude-1", + "AGENT_ROLE": "developer", + "LOG_LEVEL": "info" + } + } + } +} +``` + +### Step 5: Configure GitHub Integration (Optional) + +```json +{ + "mcpServers": { + "coorchat": { + "command": "node", + "args": [ + "C:\\projects\\coorchat\\packages\\mcp-server\\dist\\index.js" + ], + "env": { + "CHANNEL_TYPE": "redis", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "SHARED_TOKEN": "cct_YOUR_TOKEN", + "AGENT_ID": "agent-claude-1", + "AGENT_ROLE": "developer", + "GITHUB_TOKEN": "ghp_YOUR_GITHUB_PAT", + "GITHUB_OWNER": "your-org", + "GITHUB_REPO": "your-repo", + "GITHUB_WEBHOOK_SECRET": "your_webhook_secret", + "LOG_LEVEL": "info" + } + } + } +} +``` + +**Get GitHub Token:** +1. Go to https://github.com/settings/tokens +2. Generate new token (classic) +3. Select scopes: `repo`, `read:org` +4. Copy the token + +### Step 6: Restart Claude Desktop + +Close and reopen Claude Desktop to load the MCP server. + +## Verify Installation + +### Test 1: Check MCP Server is Running + +In Claude Desktop, type: +``` +Can you check if the coorchat MCP server is connected? +``` + +You should see the MCP tools available in Claude's context. + +### Test 2: Run Integration Tests + +```bash +cd packages/mcp-server +npm test +``` + +Expected output: +``` +Test Files 2 passed (2) +Tests 34 passed (34) +``` + +### Test 3: Manual Channel Test + +Create a test script `test-connection.js`: + +```javascript +import { ChannelFactory } from './dist/channels/base/ChannelFactory.js'; + +const config = { + type: 'redis', + token: 'cct_YOUR_TOKEN', + connectionParams: { + host: 'localhost', + port: 6379, + }, +}; + +const channel = ChannelFactory.create(config); +await channel.connect(); +console.log('βœ… Connected to channel!'); + +channel.onMessage((message) => { + console.log('πŸ“¨ Received:', message); +}); + +await channel.disconnect(); +``` + +Run it: +```bash +node test-connection.js +``` + +## Environment Variables Reference + +| Variable | Required | Description | Example | +|----------|----------|-------------|---------| +| `CHANNEL_TYPE` | Yes | Channel type | `redis`, `discord`, `signalr` | +| `SHARED_TOKEN` | Yes | Auth token (16+ chars) | `cct_a3f8d9...` | +| `AGENT_ID` | Yes | Unique agent identifier | `agent-claude-1` | +| `AGENT_ROLE` | Yes | Agent role | `developer`, `tester`, `architect` | +| `REDIS_HOST` | If redis | Redis hostname | `localhost` | +| `REDIS_PORT` | If redis | Redis port | `6379` | +| `REDIS_PASSWORD` | If redis | Redis password | `your_password` | +| `REDIS_TLS` | If redis | Enable TLS | `true`, `false` | +| `DISCORD_BOT_TOKEN` | If discord | Discord bot token | `MTk4...` | +| `DISCORD_CHANNEL_ID` | If discord | Discord channel ID | `123456789...` | +| `SIGNALR_HUB_URL` | If signalr | SignalR hub URL | `https://localhost:5001/agentHub` | +| `GITHUB_TOKEN` | Optional | GitHub PAT | `ghp_...` | +| `GITHUB_OWNER` | Optional | GitHub org/user | `your-org` | +| `GITHUB_REPO` | Optional | GitHub repo | `your-repo` | +| `GITHUB_WEBHOOK_SECRET` | Optional | Webhook secret | `your_secret` | +| `LOG_LEVEL` | Optional | Log verbosity | `debug`, `info`, `warn`, `error` | + +## Multi-Agent Setup (Testing Coordination) + +To test multiple agents coordinating: + +### Terminal 1: Developer Agent +```bash +cd packages/mcp-server +CHANNEL_TYPE=redis \ +REDIS_HOST=localhost \ +REDIS_PORT=6379 \ +SHARED_TOKEN=cct_YOUR_TOKEN \ +AGENT_ID=agent-dev-1 \ +AGENT_ROLE=developer \ +node dist/index.js +``` + +### Terminal 2: Tester Agent +```bash +cd packages/mcp-server +CHANNEL_TYPE=redis \ +REDIS_HOST=localhost \ +REDIS_PORT=6379 \ +SHARED_TOKEN=cct_YOUR_TOKEN \ +AGENT_ID=agent-test-1 \ +AGENT_ROLE=tester \ +node dist/index.js +``` + +### Terminal 3: Monitor Redis Messages +```bash +docker exec -it coorchat-redis redis-cli +> SUBSCRIBE coorchat:channel +``` + +## Troubleshooting + +### "Cannot find module" errors +```bash +cd packages/mcp-server +npm run build +``` + +### "ECONNREFUSED" on Redis +```bash +# Check Redis is running +docker ps | grep redis + +# Or start it +docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine +``` + +### MCP server not showing in Claude +1. Check Claude Desktop logs: `%APPDATA%\Claude\logs\` +2. Verify paths in `claude_desktop_config.json` use absolute paths +3. Ensure backslashes are escaped: `C:\\projects\\...` +4. Restart Claude Desktop completely + +### "Invalid authentication token" errors +- Ensure `SHARED_TOKEN` is the same across all agents +- Token must be 16+ characters +- Use the `cct_` prefix for channel tokens + +### GitHub integration not working +- Verify `GITHUB_TOKEN` has `repo` scope +- Check `GITHUB_OWNER` and `GITHUB_REPO` are correct +- Ensure repo exists and token has access + +## Docker Compose Quick Start + +From repo root: + +```bash +# Start everything (Redis + Relay Server) +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop everything +docker-compose down +``` + +Then configure Claude Desktop to use Redis at `localhost:6379`. + +## Next Steps + +Once installed: + +1. **Test basic coordination**: Create a task and assign it to an agent +2. **Set up GitHub sync**: Connect to a repo and sync issues +3. **Add more agents**: Create multiple Claude Desktop profiles or standalone agents +4. **Monitor coordination**: Watch agents communicate and coordinate work + +## Development Mode + +For active development: + +```bash +cd packages/mcp-server + +# Watch mode (auto-rebuild on changes) +npm run dev + +# Run tests on save +npm test -- --watch +``` + +## Uninstall + +```bash +# Remove Docker containers +docker-compose down -v +docker rm -f coorchat-redis + +# Remove MCP server from Claude config +# Edit: C:\Users\YourUser\.claude\claude_desktop_config.json +# Remove the "coorchat" entry + +# Remove node_modules +cd packages/mcp-server +rm -rf node_modules dist +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ebb97ce --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Stuart Fraser and CoorChat Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..267728b --- /dev/null +++ b/README.md @@ -0,0 +1,571 @@ +# πŸ€– CoorChat + +
+ +**Multi-Agent Coordination System for AI-Powered Software Development** + +[![Tests](https://img.shields.io/badge/tests-34%20passing-success)](./packages/mcp-server/tests) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/) +[![Node](https://img.shields.io/badge/Node-18+-green)](https://nodejs.org/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) + +*Enable teams of specialized AI agents to coordinate on shared software development tasks through secure, real-time communication channels.* + +[Quick Start](#-quick-start) β€’ [Documentation](#-documentation) β€’ [Examples](#-example-scenarios) β€’ [CLI Reference](#-cli-tools) + +
+ +--- + +## 🌟 What is CoorChat? + +CoorChat is a **multi-agent coordination platform** that allows specialized AI agents (developers, testers, architects, security auditors, etc.) to collaborate on software development tasks just like human teams do. + +### The Problem + +AI agents working in isolation can't coordinate complex workflows: +- ❌ No shared context across multiple agents +- ❌ Duplicated work or conflicting changes +- ❌ No visibility into what other agents are doing +- ❌ Manual task assignment and dependency management + +### The Solution + +CoorChat provides a **secure coordination layer** enabling: +- βœ… **Real-time communication** between specialized agents +- βœ… **Automatic task distribution** based on agent capabilities +- βœ… **Dependency tracking** and conflict resolution +- βœ… **GitHub integration** for seamless issue/PR synchronization +- βœ… **Human oversight** through monitoring and audit trails + +--- + +## ✨ Key Features + +### πŸ”„ Multi-Agent Coordination +Agents with different specializations (developer, tester, architect, security, infrastructure, documentation) work together on shared tasks with automatic role matching and capability-based assignment. + +### πŸ” Secure Communication +- Token-based authentication with timing-safe comparison +- HMAC message signing for integrity verification +- TLS/HTTPS enforcement for all channels +- Cryptographically secure token generation + +### πŸ“‘ Multiple Channel Types +Choose the communication channel that fits your infrastructure: +- **Redis** - Fast, reliable pub/sub (recommended for self-hosted) +- **Discord** - Zero setup, great for testing and small teams +- **SignalR** - Enterprise-grade .NET relay server for distributed teams + +### πŸ”— GitHub Integration +- Automatic synchronization of issues and pull requests +- Webhook + polling fallback for reliability +- Task creation from GitHub events +- Bi-directional updates (agent actions β†’ GitHub comments) + +### 🎯 Smart Task Management +- **Dependency tracking** with cycle detection +- **Conflict resolution** when multiple agents claim the same task +- **Priority queuing** (critical β†’ high β†’ medium β†’ low) +- **Lifecycle events** (assigned β†’ started β†’ blocked β†’ progress β†’ completed) + +### πŸ‘₯ Extensible Role System +8 predefined roles + custom role support: +``` +developer, tester, architect, frontend, backend, +infrastructure, security-auditor, documentation-writer +``` + +### πŸ“Š Monitoring & Observability +- Real-time activity monitoring +- Structured logging (JSON format) +- Task lifecycle tracking +- Agent timeout detection + +--- + +## πŸ—οΈ Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ GitHub Issues/PRs β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ GitHub Sync β”‚ (Webhook + Polling) + β”‚ Manager β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Task Queue β”‚ ◄──── Priority, Dependencies + β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” +β”‚ Agent β”‚ β”‚ Agent β”‚ β”‚ Agent β”‚ Developer, Tester, etc. +β”‚ 1 β”‚ β”‚ 2 β”‚ β”‚ 3 β”‚ +β””β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Communication Layer β”‚ + β”‚ (Redis/Discord/ β”‚ + β”‚ SignalR) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Components + +**MCP Server** (`packages/mcp-server/`) +- TypeScript/Node.js coordination client +- Integrates with Claude Desktop via MCP protocol +- Handles agent registration, task management, messaging + +**Relay Server** (`packages/relay-server/`) +- C#/.NET SignalR hub (optional) +- Enterprise-grade relay for distributed teams +- Token authentication middleware + +--- + +## πŸš€ Quick Start + +### Prerequisites + +- **Node.js** 18+ ([Download](https://nodejs.org/)) +- **Docker** (optional, for Redis) ([Download](https://www.docker.com/)) +- **Git** ([Download](https://git-scm.com/)) + +### Option 1: Automated Setup ⚑ (Recommended) + +**Linux/macOS:** +```bash +git clone https://github.com/stuartf303/coorchat.git +cd coorchat +chmod +x quick-start.sh +./quick-start.sh +``` + +**Windows (PowerShell):** +```powershell +git clone https://github.com/stuartf303/coorchat.git +cd coorchat +.\quick-start.ps1 +``` + +This will: +1. βœ… Install all dependencies +2. βœ… Generate secure authentication token +3. βœ… Set up your chosen channel (Redis/Discord/SignalR) +4. βœ… Run the test suite (34 tests) +5. βœ… Generate Claude Desktop configuration + +### Option 2: Manual Installation + +```bash +# 1. Clone and install +git clone https://github.com/stuartf303/coorchat.git +cd coorchat/packages/mcp-server +npm install +npm run build + +# 2. Generate secure token +npm run cli -- token generate +# Output: cct_a3f8d9e2c1b4f7a6e8d2c9b1f4a7e3d2... + +# 3. Start Redis (or use Discord/SignalR) +docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine + +# 4. Configure environment +cat > .env << EOF +CHANNEL_TYPE=redis +REDIS_HOST=localhost +REDIS_PORT=6379 +SHARED_TOKEN=cct_YOUR_TOKEN_FROM_STEP2 +AGENT_ID=agent-1 +AGENT_ROLE=developer +EOF + +# 5. Start an agent +npm run cli -- agent start --role developer +``` + +### Verify Installation + +```bash +# Run tests +npm test + +# Expected output: +# Test Files 2 passed (2) +# Tests 34 passed (34) +``` + +--- + +## 🎯 Example Scenarios + +### Scenario 1: Feature Development Workflow + +Coordinate developer, tester, and documentation agents on a new feature: + +```bash +# Terminal 1: Developer Agent +AGENT_ID=dev-1 AGENT_ROLE=developer npm run cli -- agent start + +# Terminal 2: Tester Agent +AGENT_ID=test-1 AGENT_ROLE=tester npm run cli -- agent start + +# Terminal 3: Documentation Agent +AGENT_ID=doc-1 AGENT_ROLE=documentation-writer npm run cli -- agent start + +# Terminal 4: Monitor Activity +npm run cli -- monitor +``` + +**What happens:** +1. GitHub issue created: "Add user authentication" +2. Developer agent picks up task, implements feature +3. Tester agent automatically notified when code is ready +4. Tester writes and runs tests +5. Documentation agent updates API docs +6. All agents report completion β†’ task marked done + +### Scenario 2: Bug Fix Coordination + +Critical bug workflow with automatic triage and deployment: + +```bash +# Agents automatically coordinate through these stages: +User reports bug β†’ Triage analysis β†’ Developer fixes β†’ +Tester validates β†’ Infrastructure deploys β†’ Done +``` + +**See [SCENARIOS.md](./SCENARIOS.md) for 5 complete workflow examples** including: +- Feature development +- Bug fix coordination +- Code review pipeline +- Infrastructure deployment +- Security audit + +--- + +## πŸ› οΈ CLI Tools + +CoorChat includes a full-featured CLI for managing agents, tokens, and monitoring: + +```bash +# Token Management +npm run cli -- token generate # Generate secure token +npm run cli -- token validate # Validate token format +npm run cli -- token hash # SHA-256 hash + +# Agent Control +npm run cli -- agent start --role developer +npm run cli -- agent start --id my-agent --role tester +npm run cli -- agent list # List active agents + +# Role Management +npm run cli -- role list # Show all available roles +npm run cli -- role suggest testing security # Suggest roles by capability + +# Configuration +npm run cli -- config show # Show current config +npm run cli -- config init --channel redis # Initialize config + +# Monitoring +npm run cli -- monitor # Watch real-time coordination +``` + +**Full CLI documentation:** [CLI.md](./packages/mcp-server/CLI.md) + +--- + +## πŸ“š Documentation + +### Getting Started +- **[Installation Guide](./INSTALL.md)** - Complete setup for all platforms and channels +- **[Quick Start Scripts](./quick-start.sh)** - Automated installation +- **[CLI Reference](./packages/mcp-server/CLI.md)** - Command-line tool documentation + +### Guides & Examples +- **[Example Scenarios](./SCENARIOS.md)** - Real-world coordination workflows +- **[Specifications](./specs/001-multi-agent-coordination/)** - Feature specs and design docs + +### Configuration +- **[Environment Variables](./INSTALL.md#environment-variables-reference)** - All config options +- **[Channel Setup](./INSTALL.md#step-3-choose-your-channel)** - Redis, Discord, SignalR + +### Development +- **[Project Structure](#project-structure)** - Codebase organization +- **[Contributing](#contributing)** - Development workflow +- **[Testing](#testing)** - Running tests + +--- + +## πŸ—οΈ Project Structure + +``` +coorchat/ +β”œβ”€β”€ packages/ +β”‚ β”œβ”€β”€ mcp-server/ # TypeScript/Node.js MCP Server +β”‚ β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”‚ β”œβ”€β”€ agents/ # Agent registry, roles, capabilities +β”‚ β”‚ β”‚ β”œβ”€β”€ channels/ # Discord, SignalR, Redis channels +β”‚ β”‚ β”‚ β”œβ”€β”€ cli/ # Command-line interface +β”‚ β”‚ β”‚ β”œβ”€β”€ config/ # Configuration, token generation +β”‚ β”‚ β”‚ β”œβ”€β”€ github/ # GitHub integration (webhooks, polling) +β”‚ β”‚ β”‚ β”œβ”€β”€ logging/ # Structured logging +β”‚ β”‚ β”‚ β”œβ”€β”€ protocol/ # Message protocol, validation +β”‚ β”‚ β”‚ └── tasks/ # Task queue, dependencies, conflicts +β”‚ β”‚ └── tests/ +β”‚ β”‚ └── integration/ # Integration test suites +β”‚ β”‚ +β”‚ └── relay-server/ # C#/.NET SignalR Relay (optional) +β”‚ └── src/ +β”‚ β”œβ”€β”€ Api/ # SignalR hub, middleware +β”‚ └── Core/ # Authentication service +β”‚ +β”œβ”€β”€ specs/ # Feature specifications +β”‚ └── 001-multi-agent-coordination/ +β”‚ β”œβ”€β”€ spec.md # Feature specification +β”‚ β”œβ”€β”€ plan.md # Implementation plan +β”‚ β”œβ”€β”€ data-model.md # Entity models +β”‚ β”œβ”€β”€ contracts/ # API contracts +β”‚ └── tasks.md # Task breakdown +β”‚ +β”œβ”€β”€ .github/ # CI/CD workflows +β”‚ └── workflows/ +β”‚ β”œβ”€β”€ mcp-server-ci.yml # TypeScript tests +β”‚ └── relay-server-ci.yml # C# tests +β”‚ +β”œβ”€β”€ quick-start.sh # Automated setup (Bash) +β”œβ”€β”€ quick-start.ps1 # Automated setup (PowerShell) +β”œβ”€β”€ docker-compose.yml # Local development stack +β”œβ”€β”€ INSTALL.md # Installation guide +β”œβ”€β”€ SCENARIOS.md # Example workflows +└── README.md # This file +``` + +--- + +## πŸ§ͺ Testing + +CoorChat includes comprehensive integration tests: + +```bash +cd packages/mcp-server + +# Run all tests +npm test + +# Run with coverage +npm run test:coverage + +# Run integration tests only +npm run test:integration +``` + +### Test Suites + +**Agent-Task Coordination** (10 tests) +- Agent registration and discovery +- Task assignment with role matching +- Dependency tracking and automatic unblocking +- Conflict resolution +- Lifecycle event handling +- Complete workflow orchestration + +**Secure Communication** (24 tests) +- Token generation (entropy, uniqueness, formats) +- Token validation (format, length, characters) +- Token hashing (consistency, collision resistance) +- Channel authentication (rejection, verification, timing-safety) +- Message security (metadata, integrity, tampering detection) +- Security best practices +- TLS/encryption support +- Edge cases + +**Current Status:** βœ… 34/34 tests passing + +--- + +## πŸ”’ Security Features + +### Authentication +- **Token-based authentication** with 16+ character minimum +- **Timing-safe comparison** prevents timing attacks +- **SHA-256 token hashing** for secure storage +- **Token prefixes** (cct_, cca_) for type identification + +### Message Security +- **HMAC-SHA256 signatures** for Redis message integrity +- **Correlation IDs** for request/response tracking +- **Timestamp validation** for replay attack prevention + +### Transport Security +- **TLS enforcement** for Redis (rediss://) +- **HTTPS validation** for SignalR +- **Environment-based policies** (production vs development) + +### Best Practices +- **No plaintext token storage** +- **Secure random generation** using crypto.randomBytes() +- **Automatic security warnings** for insecure configurations + +--- + +## 🎨 Use Cases + +### Software Development Teams +- Coordinate multiple AI agents on feature development +- Automated code review pipeline +- Bug triage and fix coordination +- Documentation generation + +### DevOps & Infrastructure +- Multi-stage deployment orchestration +- Infrastructure as code coordination +- Automated security audits +- Compliance checking + +### Quality Assurance +- Automated test generation +- Regression test coordination +- Coverage analysis +- Performance testing + +### Research & Experimentation +- Multi-agent AI research +- Coordination algorithm testing +- Custom workflow prototyping +- Agent capability experiments + +--- + +## πŸ—ΊοΈ Roadmap + +### Current Version: MVP (v1.0) +- βœ… Multi-agent coordination +- βœ… 3 channel types (Redis, Discord, SignalR) +- βœ… GitHub integration +- βœ… Secure authentication +- βœ… Task dependencies +- βœ… Conflict resolution +- βœ… CLI tool +- βœ… Comprehensive documentation + +### Planned Features +- πŸ”² Web dashboard for monitoring +- πŸ”² Metrics and analytics +- πŸ”² Agent performance tracking +- πŸ”² Advanced conflict resolution strategies +- πŸ”² Multi-repository coordination +- πŸ”² Slack/Teams integration +- πŸ”² Plugin system +- πŸ”² Agent marketplace + +--- + +## 🀝 Contributing + +We welcome contributions! CoorChat follows the **Specify workflow**: + +1. **Specification** - Define the feature clearly +2. **Clarification** - Resolve ambiguities +3. **Planning** - Create implementation plan +4. **Task Generation** - Break down into tasks +5. **Implementation** - Execute the plan + +### Development Setup + +```bash +# Clone and install +git clone https://github.com/stuartf303/coorchat.git +cd coorchat/packages/mcp-server +npm install + +# Run tests +npm test + +# Run in development mode +npm run dev + +# Lint and format +npm run lint +npm run format +``` + +### Pull Request Process + +1. Create a feature branch from `main` +2. Make your changes with tests +3. Ensure all tests pass (`npm test`) +4. Update documentation as needed +5. Submit PR with clear description +6. Wait for CI/CD checks +7. Address review feedback + +**See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines** + +--- + +## πŸ“„ License + +This project is licensed under the **MIT License** - see the [LICENSE](./LICENSE) file for details. + +``` +MIT License + +Copyright (c) 2026 CoorChat Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +[Full MIT License text...] +``` + +--- + +## πŸ™ Acknowledgments + +- **Claude (Anthropic)** - AI pair programming partner +- **Model Context Protocol (MCP)** - Agent integration framework +- **Specify** - Specification-driven development workflow +- **Open Source Community** - For the amazing libraries used in this project + +--- + +## πŸ“ž Support & Community + +- **Documentation**: [Read the docs](./INSTALL.md) +- **Issues**: [Report bugs or request features](https://github.com/stuartf303/coorchat/issues) +- **Discussions**: [Ask questions or share ideas](https://github.com/stuartf303/coorchat/discussions) + +--- + +## ⭐ Star History + +If you find CoorChat useful, please consider giving it a star! ⭐ + +[![Star History](https://api.star-history.com/svg?repos=stuartf303/coorchat&type=Date)](https://star-history.com/#stuartf303/coorchat&Date) + +--- + +
+ +**Made with ❀️ by developers, for developers** + +[⬆ Back to Top](#-coorchat) + +
diff --git a/SCENARIOS.md b/SCENARIOS.md new file mode 100644 index 0000000..75549fc --- /dev/null +++ b/SCENARIOS.md @@ -0,0 +1,622 @@ +# CoorChat Example Scenarios + +Real-world examples of multi-agent coordination using CoorChat. + +## Table of Contents + +1. [Scenario 1: Feature Development Workflow](#scenario-1-feature-development-workflow) +2. [Scenario 2: Bug Fix Coordination](#scenario-2-bug-fix-coordination) +3. [Scenario 3: Code Review Pipeline](#scenario-3-code-review-pipeline) +4. [Scenario 4: Infrastructure Deployment](#scenario-4-infrastructure-deployment) +5. [Scenario 5: Security Audit](#scenario-5-security-audit) +6. [Running These Scenarios](#running-these-scenarios) + +--- + +## Scenario 1: Feature Development Workflow + +**Goal**: Coordinate development, testing, and documentation for a new feature + +**Agents Involved**: +- Developer Agent (implements feature) +- Tester Agent (writes and runs tests) +- Documentation Writer (updates docs) +- Architect (reviews design) + +### Setup + +```bash +# Terminal 1: Start Redis +docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine + +# Generate shared token +TOKEN=$(node -e "console.log('cct_' + require('crypto').randomBytes(32).toString('hex'))") +echo "Shared Token: $TOKEN" +``` + +### Run Agents + +```bash +# Terminal 2: Developer Agent +cd packages/mcp-server +CHANNEL_TYPE=redis \ +REDIS_HOST=localhost \ +REDIS_PORT=6379 \ +SHARED_TOKEN=$TOKEN \ +AGENT_ID=dev-agent-1 \ +AGENT_ROLE=developer \ +npm run cli -- agent start --role developer + +# Terminal 3: Tester Agent +CHANNEL_TYPE=redis \ +REDIS_HOST=localhost \ +REDIS_PORT=6379 \ +SHARED_TOKEN=$TOKEN \ +AGENT_ID=test-agent-1 \ +AGENT_ROLE=tester \ +npm run cli -- agent start --role tester + +# Terminal 4: Documentation Agent +CHANNEL_TYPE=redis \ +REDIS_HOST=localhost \ +REDIS_PORT=6379 \ +SHARED_TOKEN=$TOKEN \ +AGENT_ID=doc-agent-1 \ +AGENT_ROLE=documentation-writer \ +npm run cli -- agent start --role documentation-writer + +# Terminal 5: Monitor Activity +npm run cli -- monitor +``` + +### Workflow + +1. **GitHub Issue Created**: `Add user authentication feature` +2. **Architect Agent** reviews requirements, creates technical spec +3. **Developer Agent** picks up task, implements authentication +4. **Tester Agent** automatically notified when code is ready +5. **Tester Agent** writes tests, runs them +6. **Documentation Agent** updates API docs +7. **All agents** report completion, task marked done + +### Expected Message Flow + +``` +[09:00:00] TASK_ASSIGNED + From: github-sync + To: dev-agent-1 + Payload: { + "taskId": "issue-123", + "description": "Add user authentication", + "githubIssue": "https://github.com/org/repo/issues/123" + } + +[09:15:00] TASK_STARTED + From: dev-agent-1 + Payload: { + "taskId": "issue-123", + "status": "in_progress" + } + +[10:30:00] TASK_PROGRESS + From: dev-agent-1 + Payload: { + "taskId": "issue-123", + "message": "Authentication endpoints implemented", + "completionPercentage": 60 + } + +[11:00:00] TASK_COMPLETED + From: dev-agent-1 + Payload: { + "taskId": "issue-123", + "branch": "feature/user-auth", + "pullRequest": "https://github.com/org/repo/pull/456" + } + +[11:00:01] TASK_ASSIGNED + From: task-queue + To: test-agent-1 + Payload: { + "taskId": "test-issue-123", + "dependsOn": "issue-123", + "testTarget": "feature/user-auth" + } +``` + +--- + +## Scenario 2: Bug Fix Coordination + +**Goal**: Quickly triage, fix, test, and deploy a critical bug + +**Agents Involved**: +- Triage Agent (analyzes bug reports) +- Developer Agent (fixes bug) +- Tester Agent (regression testing) +- Infrastructure Agent (hotfix deployment) + +### Setup + +```bash +# Use GitHub integration for automatic bug sync +GITHUB_TOKEN=ghp_your_token +GITHUB_OWNER=your-org +GITHUB_REPO=your-repo +GITHUB_WEBHOOK_SECRET=$(openssl rand -hex 32) +``` + +### Workflow + +1. **User reports bug** via GitHub issue with label `bug` and `priority:critical` +2. **Triage Agent** automatically assigned, analyzes stack trace +3. **Developer Agent** receives assignment with triage analysis +4. **Developer Agent** creates hotfix branch, fixes bug +5. **Tester Agent** runs regression test suite +6. **Infrastructure Agent** deploys hotfix to production +7. **All agents** notify completion, GitHub issue auto-closed + +### Test Script + +Create `scenarios/bug-fix-test.ts`: + +```typescript +import { TaskQueue } from '../src/tasks/TaskQueue.js'; +import { AgentRegistry } from '../src/agents/AgentRegistry.js'; +import { Task } from '../src/tasks/Task.js'; + +// Simulate critical bug workflow +const queue = new TaskQueue(); +const registry = new AgentRegistry(); + +// Register agents +const triageAgent = registry.registerAgent({ + id: 'triage-agent-1', + role: 'tester', + capabilities: ['bug-triage', 'log-analysis'], + status: 'active', + metadata: { specialization: 'triage' }, +}); + +const devAgent = registry.registerAgent({ + id: 'dev-agent-1', + role: 'developer', + capabilities: ['javascript', 'typescript', 'bugfix'], + status: 'active', + metadata: {}, +}); + +// Create critical bug task +const bugTask: Task = { + id: 'bug-critical-001', + description: 'Fix: Payment processing timeout', + requiredCapabilities: ['bug-triage'], + priority: 'critical', + status: 'pending', + createdAt: new Date(), + metadata: { + githubIssue: 'https://github.com/org/repo/issues/789', + errorMessage: 'Timeout after 30s', + affectedUsers: 1523, + }, +}; + +// Add to queue +await queue.addTask(bugTask); + +// Triage agent analyzes +const assigned = await queue.assignTask(triageAgent.id); +console.log('Bug assigned to triage:', assigned); + +// Create fix task after triage +const fixTask: Task = { + id: 'bug-fix-001', + description: 'Implement fix for payment timeout', + requiredCapabilities: ['bugfix', 'javascript'], + priority: 'critical', + status: 'pending', + dependencies: ['bug-critical-001'], + createdAt: new Date(), + metadata: { + triageAnalysis: 'Database connection pool exhaustion', + suggestedFix: 'Increase pool size and add timeout handling', + }, +}; + +await queue.addTask(fixTask); +``` + +--- + +## Scenario 3: Code Review Pipeline + +**Goal**: Automated code review coordination + +**Agents Involved**: +- Security Auditor (checks for vulnerabilities) +- Code Reviewer (style and best practices) +- Test Agent (coverage validation) +- Architect (design review) + +### Workflow + +1. **Pull Request created** on GitHub +2. **Security Auditor** scans for common vulnerabilities (SQL injection, XSS, etc.) +3. **Code Reviewer** checks code style, naming conventions +4. **Test Agent** validates 80%+ code coverage +5. **Architect** reviews architectural changes +6. **All agents** must approve before merge + +### Configuration + +Create `scenarios/code-review-config.json`: + +```json +{ + "reviewPipeline": { + "requiredReviewers": [ + { + "agentRole": "security-auditor", + "checks": ["owasp-top-10", "dependency-scan", "secrets-detection"] + }, + { + "agentRole": "developer", + "checks": ["code-style", "naming-conventions", "complexity"] + }, + { + "agentRole": "tester", + "checks": ["coverage-threshold", "test-quality"] + }, + { + "agentRole": "architect", + "checks": ["design-patterns", "architecture-compliance"], + "requiredForFiles": ["src/core/**", "src/api/**"] + } + ], + "approvalThreshold": "all", + "autoMerge": false + } +} +``` + +### Implementation + +```typescript +// File: scenarios/code-review.ts +import { WebhookHandler } from '../src/github/WebhookHandler.js'; +import { TaskQueue } from '../src/tasks/TaskQueue.js'; + +const webhookHandler = new WebhookHandler({ + port: 3000, + path: '/webhook', + secret: process.env.GITHUB_WEBHOOK_SECRET!, +}); + +webhookHandler.on('pull_request.opened', async (payload) => { + const pr = payload.pull_request; + + // Create review tasks for each reviewer type + const reviewTasks = [ + { + id: `security-review-${pr.number}`, + description: `Security review for PR #${pr.number}`, + requiredCapabilities: ['security-audit', 'vulnerability-scan'], + priority: 'high' as const, + status: 'pending' as const, + createdAt: new Date(), + metadata: { + prNumber: pr.number, + prUrl: pr.html_url, + reviewType: 'security', + }, + }, + { + id: `code-review-${pr.number}`, + description: `Code style review for PR #${pr.number}`, + requiredCapabilities: ['code-review', 'style-check'], + priority: 'medium' as const, + status: 'pending' as const, + createdAt: new Date(), + metadata: { + prNumber: pr.number, + prUrl: pr.html_url, + reviewType: 'code-quality', + }, + }, + { + id: `test-review-${pr.number}`, + description: `Test coverage review for PR #${pr.number}`, + requiredCapabilities: ['testing', 'coverage-analysis'], + priority: 'medium' as const, + status: 'pending' as const, + createdAt: new Date(), + metadata: { + prNumber: pr.number, + prUrl: pr.html_url, + reviewType: 'test-coverage', + coverageThreshold: 80, + }, + }, + ]; + + // Add all review tasks + const queue = new TaskQueue(); + for (const task of reviewTasks) { + await queue.addTask(task); + } + + console.log(`Created ${reviewTasks.length} review tasks for PR #${pr.number}`); +}); + +await webhookHandler.start(); +``` + +--- + +## Scenario 4: Infrastructure Deployment + +**Goal**: Coordinate multi-stage deployment with validation + +**Agents Involved**: +- Backend Developer (API deployment) +- Frontend Developer (UI deployment) +- Infrastructure Agent (Kubernetes/Docker) +- Tester Agent (smoke tests) + +### Workflow + +``` +1. Backend Agent deploys API to staging + ↓ +2. Infrastructure Agent validates health checks + ↓ +3. Frontend Agent deploys UI to staging + ↓ +4. Tester Agent runs smoke tests + ↓ (if tests pass) +5. Infrastructure Agent promotes to production + ↓ +6. Tester Agent runs production smoke tests + ↓ +7. All agents report success +``` + +### Dependency Chain + +```typescript +import { DependencyTracker } from '../src/tasks/DependencyTracker.js'; + +const tracker = new DependencyTracker(); + +// Define deployment tasks with dependencies +const tasks = [ + { id: 'deploy-api-staging', dependencies: [] }, + { id: 'validate-api-health', dependencies: ['deploy-api-staging'] }, + { id: 'deploy-ui-staging', dependencies: ['validate-api-health'] }, + { id: 'run-smoke-tests', dependencies: ['deploy-ui-staging'] }, + { id: 'deploy-api-prod', dependencies: ['run-smoke-tests'] }, + { id: 'deploy-ui-prod', dependencies: ['deploy-api-prod'] }, + { id: 'run-prod-smoke-tests', dependencies: ['deploy-ui-prod'] }, +]; + +// Add all dependencies +for (const task of tasks) { + for (const dep of task.dependencies) { + tracker.addDependency(task.id, dep); + } +} + +// Check which tasks are ready +const ready = tracker.getReadyTasks(); +console.log('Tasks ready to execute:', ready); + +// Mark task complete and get newly unblocked tasks +const unblocked = tracker.markCompleted('deploy-api-staging'); +console.log('Newly unblocked tasks:', unblocked); +``` + +--- + +## Scenario 5: Security Audit + +**Goal**: Comprehensive security review of codebase + +**Agents Involved**: +- Security Auditor (vulnerability scanning) +- Developer (fix implementation) +- Tester (security test validation) +- Documentation Writer (security docs) + +### Workflow + +1. **Security Auditor** scans entire codebase +2. **Creates tasks** for each finding (by severity) +3. **Developer Agents** assigned based on file ownership +4. **Each fix** reviewed by Security Auditor +5. **Tester Agent** validates fixes don't introduce regressions +6. **Documentation Agent** updates security guidelines + +### Security Scan Example + +```typescript +// File: scenarios/security-audit.ts +import { TaskQueue } from '../src/tasks/TaskQueue.js'; +import { Task } from '../src/tasks/Task.js'; + +interface SecurityFinding { + severity: 'critical' | 'high' | 'medium' | 'low'; + category: string; + file: string; + line: number; + description: string; + recommendation: string; +} + +const findings: SecurityFinding[] = [ + { + severity: 'critical', + category: 'SQL Injection', + file: 'src/database/queries.ts', + line: 45, + description: 'Unsanitized user input in SQL query', + recommendation: 'Use parameterized queries', + }, + { + severity: 'high', + category: 'XSS', + file: 'src/ui/UserProfile.tsx', + line: 123, + description: 'Unescaped user content rendered', + recommendation: 'Use DOMPurify or framework escaping', + }, + // ... more findings +]; + +// Create tasks for each finding +const queue = new TaskQueue(); + +for (const finding of findings) { + const task: Task = { + id: `security-fix-${finding.file}-${finding.line}`, + description: `[${finding.severity.toUpperCase()}] Fix ${finding.category} in ${finding.file}`, + requiredCapabilities: ['security', finding.category.toLowerCase()], + priority: finding.severity === 'critical' ? 'critical' : 'high', + status: 'pending', + createdAt: new Date(), + metadata: { + securityFinding: finding, + file: finding.file, + line: finding.line, + recommendation: finding.recommendation, + }, + }; + + await queue.addTask(task); +} + +console.log(`Created ${findings.length} security fix tasks`); +``` + +--- + +## Running These Scenarios + +### Option 1: Automated Test Suite + +```bash +# Run all scenario tests +cd packages/mcp-server +npm run scenarios + +# Run specific scenario +npm run scenario -- code-review +``` + +### Option 2: Interactive Demo + +```bash +# Start demo environment +./scripts/demo-setup.sh + +# This will: +# 1. Start Redis +# 2. Start 4 agents (developer, tester, architect, security) +# 3. Load example GitHub issues +# 4. Show real-time coordination +``` + +### Option 3: Manual Execution + +```bash +# Terminal 1: Start infrastructure +docker-compose up -d + +# Terminal 2-5: Start agents +npm run cli -- agent start --role developer +npm run cli -- agent start --role tester +npm run cli -- agent start --role security-auditor +npm run cli -- agent start --role architect + +# Terminal 6: Monitor +npm run cli -- monitor + +# Terminal 7: Trigger scenario +node scenarios/feature-development.js +``` + +--- + +## Scenario Metrics + +Track coordination effectiveness: + +```typescript +interface ScenarioMetrics { + totalTasks: number; + completedTasks: number; + averageTaskTime: number; // milliseconds + agentUtilization: Record; // percentage + taskSuccessRate: number; // percentage + coordinationOverhead: number; // milliseconds (avg message latency) +} + +// Example output: +{ + totalTasks: 15, + completedTasks: 15, + averageTaskTime: 45000, // 45 seconds + agentUtilization: { + 'dev-agent-1': 85, + 'test-agent-1': 60, + 'security-agent-1': 40, + }, + taskSuccessRate: 100, + coordinationOverhead: 120, // 120ms average +} +``` + +--- + +## Custom Scenarios + +Create your own scenario: + +```typescript +// File: scenarios/my-scenario.ts +import { ScenarioRunner } from './utils/ScenarioRunner.js'; + +const scenario = new ScenarioRunner({ + name: 'My Custom Workflow', + agents: [ + { role: 'developer', count: 2 }, + { role: 'tester', count: 1 }, + ], + tasks: [ + { + description: 'Implement feature X', + assignTo: 'developer', + dependencies: [], + }, + { + description: 'Test feature X', + assignTo: 'tester', + dependencies: ['Implement feature X'], + }, + ], +}); + +await scenario.run(); +scenario.printMetrics(); +``` + +--- + +## Next Steps + +1. **Try the scenarios** - Start with Scenario 1 (Feature Development) +2. **Monitor activity** - Use `npm run cli -- monitor` to watch coordination +3. **Customize workflows** - Modify scenarios for your use case +4. **Measure performance** - Track metrics to optimize coordination +5. **Scale up** - Add more agents and parallel workflows + +Happy coordinating! πŸ€– diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1b51003 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + mcp-server: + build: + context: ./packages/mcp-server + dockerfile: Dockerfile + environment: + - NODE_ENV=development + - DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} + - CHANNEL_TOKEN=${CHANNEL_TOKEN} + - GITHUB_TOKEN=${GITHUB_TOKEN} + - GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET} + volumes: + - ./.coorchat:/app/.coorchat + - ./packages/mcp-server/src:/app/src + ports: + - "3000:3000" + restart: unless-stopped + + # Optional: Relay Server (C#/.NET component) + # relay-server: + # build: + # context: ./packages/relay-server + # dockerfile: Dockerfile + # environment: + # - ASPNETCORE_ENVIRONMENT=Development + # - ConnectionStrings__DefaultConnection=${DATABASE_URL} + # ports: + # - "5000:80" + # restart: unless-stopped + + # Optional: Redis (if using Redis channel) + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD:-changeme} + ports: + - "6379:6379" + volumes: + - redis-data:/data + +volumes: + redis-data: diff --git a/packages/mcp-server/CLI.md b/packages/mcp-server/CLI.md new file mode 100644 index 0000000..a6266ee --- /dev/null +++ b/packages/mcp-server/CLI.md @@ -0,0 +1,516 @@ +# CoorChat CLI Documentation + +Command-line interface for managing CoorChat agents, tokens, and monitoring coordination. + +## Installation + +```bash +# From source +cd packages/mcp-server +npm install +npm run build + +# Use locally +npm run cli -- + +# Or install globally +npm install -g . +coorchat +``` + +## Commands + +### Token Management + +#### `token generate` + +Generate secure authentication tokens. + +```bash +# Generate channel token (default) +npm run cli -- token generate + +# Generate API token +npm run cli -- token generate --type api + +# Generate webhook secret +npm run cli -- token generate --type webhook + +# Generate multiple tokens +npm run cli -- token generate --count 5 +``` + +**Options**: +- `-t, --type `: Token type (`channel`, `api`, `webhook`) - default: `channel` +- `-c, --count `: Number of tokens to generate - default: `1` + +**Output**: +``` +Generated tokens: +1. cct_a3f8d9e2c1b4f7a6e8d2c9b1f4a7e3d2c6b9f1e4a8d3c7b2f5e9a1d4c8b3f6 + +Add to your .env file: +SHARED_TOKEN=cct_a3f8d9e2c1b4f7a6e8d2c9b1f4a7e3d2c6b9f1e4a8d3c7b2f5e9a1d4c8b3f6 +``` + +#### `token validate ` + +Validate token format and security requirements. + +```bash +npm run cli -- token validate cct_abc123def456 +``` + +**Output**: +``` +βœ… Token is valid +Length: 71 characters +Type: Channel Token +``` + +#### `token hash ` + +Generate SHA-256 hash of a token for secure storage. + +```bash +npm run cli -- token hash cct_your_token +``` + +**Output**: +``` +Token hash: +9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 +``` + +--- + +### Agent Management + +#### `agent start` + +Start an agent and connect to the coordination channel. + +```bash +# Start with defaults +npm run cli -- agent start + +# Specify role +npm run cli -- agent start --role tester + +# Specify agent ID +npm run cli -- agent start --id my-agent-1 --role developer + +# Specify channel type +npm run cli -- agent start --channel discord --role architect +``` + +**Options**: +- `-i, --id `: Agent ID (default: auto-generated) +- `-r, --role `: Agent role (default: `developer`) +- `-c, --channel `: Channel type (default: from env or `redis`) + +**Example**: +```bash +SHARED_TOKEN=cct_your_token \ +REDIS_HOST=localhost \ +REDIS_PORT=6379 \ +npm run cli -- agent start --role developer +``` + +**Output**: +``` +πŸ€– Starting agent: agent-1708012345678 + Role: developer + Channel: redis + +βœ… Connected to channel + +Agent is running. Press Ctrl+C to stop. + +πŸ“¨ [TASK_ASSIGNED] from task-queue + { + "taskId": "issue-123", + "description": "Implement authentication" + } +``` + +#### `agent list` + +List all active agents (requires shared state store). + +```bash +npm run cli -- agent list +``` + +--- + +### Role Management + +#### `role list` + +List all available predefined roles and their capabilities. + +```bash +npm run cli -- role list +``` + +**Output**: +``` +πŸ“‹ Available Roles: + +developer: + Description: Software developer for implementing features + Capabilities: coding, debugging, code-review, git-operations + +tester: + Description: Quality assurance and testing specialist + Capabilities: testing, test-automation, quality-assurance, bug-reporting + +architect: + Description: System architect for design and architecture decisions + Capabilities: architecture-design, system-design, technical-planning + +... (8 total roles) +``` + +#### `role suggest ` + +Suggest roles based on required capabilities. + +```bash +npm run cli -- role suggest testing code-review + +npm run cli -- role suggest security penetration-testing +``` + +**Output**: +``` +πŸ’‘ Suggested Roles: + +1. tester + Quality assurance and testing specialist + +2. developer + Software developer for implementing features +``` + +--- + +### Configuration + +#### `config show` + +Display current configuration from environment variables. + +```bash +npm run cli -- config show +``` + +**Output**: +``` +βš™οΈ Current Configuration: + +Channel Type: redis +Agent ID: agent-claude-1 +Agent Role: developer +Shared Token: ***b3f6a1d4 + +Redis Configuration: + Host: localhost + Port: 6379 + TLS: false + +GitHub Integration: + Token: ***ab12cd34 + Owner: your-org + Repo: your-repo +``` + +#### `config init` + +Generate a configuration template. + +```bash +# Generate for Redis +npm run cli -- config init --channel redis + +# Generate for Discord +npm run cli -- config init --channel discord + +# Generate for SignalR +npm run cli -- config init --channel signalr +``` + +**Output**: +``` +# CoorChat Configuration +# Generated: 2026-02-15T00:00:00.000Z + +# Shared authentication token (use same token for all agents) +SHARED_TOKEN=cct_a3f8d9e2c1b4f7a6e8d2c9b1f4a7e3d2 + +# Channel configuration +CHANNEL_TYPE=redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_TLS=false + +# Agent configuration +AGENT_ID=agent-1708012345678 +AGENT_ROLE=developer + +# Optional: GitHub integration +# GITHUB_TOKEN=ghp_your_token_here +# GITHUB_OWNER=your-org +# GITHUB_REPO=your-repo + +# Logging +LOG_LEVEL=info + +πŸ’Ύ Save this to .env file in packages/mcp-server/ +``` + +--- + +### Monitoring + +#### `monitor` + +Monitor real-time agent coordination activity. + +```bash +npm run cli -- monitor + +# Specify channel +CHANNEL_TYPE=discord \ +DISCORD_BOT_TOKEN=your_token \ +npm run cli -- monitor --channel discord +``` + +**Options**: +- `-c, --channel `: Channel type (default: from env or `redis`) + +**Output**: +``` +πŸ‘οΈ CoorChat Monitor + +Listening for agent activity... + +βœ… Connected to redis channel + +[10:30:45] TASK_ASSIGNED + From: github-sync + To: dev-agent-1 + Payload: { + "taskId": "issue-123", + "description": "Add user authentication" + } + +[10:30:50] TASK_STARTED + From: dev-agent-1 + Payload: { + "taskId": "issue-123", + "status": "in_progress" + } + +[10:45:20] TASK_COMPLETED + From: dev-agent-1 + Payload: { + "taskId": "issue-123", + "branch": "feature/user-auth" + } +``` + +Press `Ctrl+C` to stop monitoring. + +--- + +## Environment Variables + +All CLI commands respect these environment variables: + +### Required + +| Variable | Description | Example | +|----------|-------------|---------| +| `SHARED_TOKEN` | Authentication token (16+ chars) | `cct_a3f8d9e2...` | +| `CHANNEL_TYPE` | Channel type | `redis`, `discord`, `signalr` | + +### Channel-Specific + +**Redis**: +| Variable | Description | Default | +|----------|-------------|---------| +| `REDIS_HOST` | Redis hostname | `localhost` | +| `REDIS_PORT` | Redis port | `6379` | +| `REDIS_PASSWORD` | Redis password | _(none)_ | +| `REDIS_TLS` | Enable TLS | `false` | + +**Discord**: +| Variable | Description | +|----------|-------------| +| `DISCORD_BOT_TOKEN` | Discord bot token | +| `DISCORD_CHANNEL_ID` | Discord channel ID | + +**SignalR**: +| Variable | Description | Default | +|----------|-------------|---------| +| `SIGNALR_HUB_URL` | Hub URL | `https://localhost:5001/agentHub` | + +### Optional + +| Variable | Description | Default | +|----------|-------------|---------| +| `AGENT_ID` | Agent identifier | Auto-generated | +| `AGENT_ROLE` | Agent role | `developer` | +| `LOG_LEVEL` | Log verbosity | `info` | +| `GITHUB_TOKEN` | GitHub PAT | _(none)_ | +| `GITHUB_OWNER` | GitHub org/user | _(none)_ | +| `GITHUB_REPO` | GitHub repo | _(none)_ | + +--- + +## Common Workflows + +### 1. Quick Start (Local Testing) + +```bash +# Generate token +npm run cli -- token generate + +# Save output to .env +echo "SHARED_TOKEN=cct_..." > .env +echo "CHANNEL_TYPE=redis" >> .env + +# Start Redis +docker run -d -p 6379:6379 redis:7-alpine + +# Start agent +npm run cli -- agent start --role developer +``` + +### 2. Multi-Agent Coordination + +```bash +# Terminal 1: Developer +AGENT_ID=dev-1 npm run cli -- agent start --role developer + +# Terminal 2: Tester +AGENT_ID=test-1 npm run cli -- agent start --role tester + +# Terminal 3: Monitor +npm run cli -- monitor +``` + +### 3. GitHub Integration + +```bash +# Generate tokens +GITHUB_TOKEN=ghp_your_token +WEBHOOK_SECRET=$(npm run cli -- token generate --type webhook | tail -1) + +# Configure .env +cat >> .env << EOF +GITHUB_TOKEN=$GITHUB_TOKEN +GITHUB_OWNER=your-org +GITHUB_REPO=your-repo +GITHUB_WEBHOOK_SECRET=$WEBHOOK_SECRET +EOF + +# Start agent with GitHub sync +npm run cli -- agent start --role developer +``` + +### 4. Production Deployment + +```bash +# Use environment-specific config +export NODE_ENV=production +export CHANNEL_TYPE=signalr +export SIGNALR_HUB_URL=https://coorchat.example.com/hub +export SHARED_TOKEN=cct_production_token + +# Start agent +npm run cli -- agent start \ + --id prod-agent-1 \ + --role developer +``` + +--- + +## Troubleshooting + +### "Invalid or missing SHARED_TOKEN" + +```bash +# Generate new token +npm run cli -- token generate + +# Add to environment +export SHARED_TOKEN=cct_your_generated_token +``` + +### "Connection refused" (Redis) + +```bash +# Check Redis is running +docker ps | grep redis + +# Start Redis if not running +docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine +``` + +### "Cannot find module" + +```bash +# Rebuild project +npm run build +``` + +### Agent not receiving messages + +```bash +# Check token matches across all agents +npm run cli -- config show + +# Verify channel connection +npm run cli -- monitor +``` + +--- + +## Programmatic Usage + +Use the CLI programmatically in your Node.js scripts: + +```typescript +import { TokenGenerator } from '@coorchat/mcp-server/config/TokenGenerator'; +import { ChannelFactory } from '@coorchat/mcp-server/channels/base/ChannelFactory'; + +// Generate token +const token = TokenGenerator.generateChannelToken(); + +// Create channel +const channel = ChannelFactory.create({ + type: 'redis', + token, + connectionParams: { + host: 'localhost', + port: 6379, + }, +}); + +// Connect and listen +await channel.connect(); +channel.onMessage((message) => { + console.log('Received:', message); +}); +``` + +--- + +## See Also + +- [Installation Guide](../../INSTALL.md) - Full installation instructions +- [Scenarios](../../SCENARIOS.md) - Example coordination workflows +- [README](../../README.md) - Project overview diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile new file mode 100644 index 0000000..4a8e663 --- /dev/null +++ b/packages/mcp-server/Dockerfile @@ -0,0 +1,42 @@ +# Multi-stage build for CoorChat MCP Server +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build TypeScript +RUN npm run build + +# Production image +FROM node:18-alpine + +WORKDIR /app + +# Copy built application +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ + +# Create non-root user +RUN addgroup -g 1001 -S coorchat && \ + adduser -S coorchat -u 1001 && \ + chown -R coorchat:coorchat /app + +USER coorchat + +# Expose port (if needed for webhooks) +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "process.exit(0)" + +CMD ["node", "dist/index.js"] diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json new file mode 100644 index 0000000..34e5827 --- /dev/null +++ b/packages/mcp-server/package-lock.json @@ -0,0 +1,6579 @@ +{ + "name": "@coorchat/mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@coorchat/mcp-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@microsoft/signalr": "^8.0.0", + "@octokit/rest": "^20.0.2", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "commander": "^12.1.0", + "crypto": "^1.0.1", + "discord.js": "^14.14.1", + "dotenv": "^16.6.1", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "ws": "^8.16.0", + "yaml": "^2.3.4", + "zod": "^3.22.4" + }, + "bin": { + "coorchat": "dist/cli/index.js" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.11.5", + "@types/uuid": "^9.0.7", + "@types/ws": "^8.5.10", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "@typescript-eslint/parser": "^6.19.0", + "@vitest/coverage-v8": "^1.2.1", + "eslint": "^8.56.0", + "prettier": "^3.2.4", + "tsc-alias": "^1.8.8", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^1.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@ioredis/commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "license": "MIT" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/signalr": { + "version": "8.0.17", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.17.tgz", + "integrity": "sha512-5pM6xPtKZNJLO0Tq5nQasVyPFwi/WBY3QB5uc/v3dIPTpS1JXQbaXAQAPxFoQ5rTBFE094w8bbqkp17F9ReQvA==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/coverage-v8": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", + "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.1" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.39", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.39.tgz", + "integrity": "sha512-XRdDQvZvID1XvcFftjSmd4dcmMi/RL/jSy5sduBDAvCGFcNFHThdIQXCEBDZFe52lCNEzuIL0QJoKYAmRmxLUA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", + "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsc-alias/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 0000000..84864ab --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,67 @@ +{ + "name": "@coorchat/mcp-server", + "version": "1.0.0", + "description": "Multi-Agent Coordination System - MCP Server Component", + "main": "dist/index.js", + "type": "module", + "bin": { + "coorchat": "dist/cli/index.js" + }, + "scripts": { + "build": "tsc && tsc-alias", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js", + "cli": "tsx src/cli/index.ts", + "test": "vitest", + "test:integration": "vitest --config vitest.integration.config.ts", + "test:coverage": "vitest --coverage", + "lint": "eslint src/ --ext .ts", + "format": "prettier --write \"src/**/*.ts\"" + }, + "keywords": [ + "multi-agent", + "coordination", + "ai-agents", + "mcp", + "discord", + "signalr", + "redis" + ], + "author": "CoorChat", + "license": "MIT", + "dependencies": { + "@microsoft/signalr": "^8.0.0", + "@octokit/rest": "^20.0.2", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "commander": "^12.1.0", + "crypto": "^1.0.1", + "discord.js": "^14.14.1", + "dotenv": "^16.6.1", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "ws": "^8.16.0", + "yaml": "^2.3.4", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.11.5", + "@types/uuid": "^9.0.7", + "@types/ws": "^8.5.10", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "@typescript-eslint/parser": "^6.19.0", + "@vitest/coverage-v8": "^1.2.1", + "eslint": "^8.56.0", + "prettier": "^3.2.4", + "tsc-alias": "^1.8.8", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^1.2.1" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/mcp-server/src/agents/Agent.ts b/packages/mcp-server/src/agents/Agent.ts new file mode 100644 index 0000000..71e3ff6 --- /dev/null +++ b/packages/mcp-server/src/agents/Agent.ts @@ -0,0 +1,232 @@ +/** + * Agent - Represents a specialized AI agent participating in coordination + * Based on specs/001-multi-agent-coordination/data-model.md + */ + +import { Capability, Platform } from './Capability.js'; + +/** + * Agent connection status + */ +export enum AgentStatus { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', +} + +/** + * Agent entity + */ +export interface Agent { + /** Unique agent identifier (UUID v4) */ + id: string; + + /** Agent role type (extensible: developer, tester, architect, custom roles) */ + role: string; + + /** Operating system platform */ + platform: Platform; + + /** Execution environment (local, GitHub Actions, Azure DevOps, AWS, etc.) */ + environment: string; + + /** Agent capability set */ + capabilities: Capability; + + /** Connection status */ + status: AgentStatus; + + /** ID of currently assigned task (optional) */ + currentTask?: string | null; + + /** When agent joined the channel */ + registeredAt: Date; + + /** Last activity timestamp */ + lastSeenAt: Date; +} + +/** + * Agent registration data (for initial join) + */ +export interface AgentRegistration { + /** Agent role type */ + role: string; + + /** Operating system platform */ + platform: Platform; + + /** Execution environment */ + environment: string; + + /** Agent capabilities */ + capabilities: Capability; +} + +/** + * Agent update data + */ +export interface AgentUpdate { + /** Update connection status */ + status?: AgentStatus; + + /** Update current task */ + currentTask?: string | null; + + /** Update last seen timestamp */ + lastSeenAt?: Date; + + /** Update capabilities */ + capabilities?: Partial; +} + +/** + * Agent query filter + */ +export interface AgentQuery { + /** Filter by role type */ + role?: string; + + /** Filter by platform */ + platform?: Platform; + + /** Filter by environment type */ + environment?: string; + + /** Filter by connection status */ + status?: AgentStatus; + + /** Filter by whether agent has current task */ + hasCurrentTask?: boolean; + + /** Filter by minimum last seen timestamp */ + lastSeenSince?: Date; +} + +/** + * Validate agent object + */ +export function validateAgent(agent: unknown): agent is Agent { + if (typeof agent !== 'object' || agent === null) { + return false; + } + + const a = agent as Partial; + + return ( + typeof a.id === 'string' && + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + a.id + ) && + typeof a.role === 'string' && + a.role.length > 0 && + a.role.length <= 50 && + typeof a.platform === 'string' && + ['Linux', 'macOS', 'Windows'].includes(a.platform) && + typeof a.environment === 'string' && + a.environment.length > 0 && + a.environment.length <= 100 && + typeof a.capabilities === 'object' && + typeof a.status === 'string' && + Object.values(AgentStatus).includes(a.status as AgentStatus) && + a.registeredAt instanceof Date && + a.lastSeenAt instanceof Date + ); +} + +/** + * Check if an agent matches a query + */ +export function matchesAgentQuery(agent: Agent, query: AgentQuery): boolean { + if (query.role && agent.role !== query.role) { + return false; + } + + if (query.platform && agent.platform !== query.platform) { + return false; + } + + if (query.environment && agent.environment !== query.environment) { + return false; + } + + if (query.status && agent.status !== query.status) { + return false; + } + + if (query.hasCurrentTask !== undefined) { + const hasTask = agent.currentTask !== null && agent.currentTask !== undefined; + if (query.hasCurrentTask !== hasTask) { + return false; + } + } + + if (query.lastSeenSince && agent.lastSeenAt < query.lastSeenSince) { + return false; + } + + return true; +} + +/** + * Create an agent from registration data + */ +export function createAgent(id: string, registration: AgentRegistration): Agent { + const now = new Date(); + + return { + id, + role: registration.role, + platform: registration.platform, + environment: registration.environment, + capabilities: registration.capabilities, + status: AgentStatus.CONNECTING, + currentTask: null, + registeredAt: now, + lastSeenAt: now, + }; +} + +/** + * Update an agent with partial data + */ +export function updateAgent(agent: Agent, update: AgentUpdate): Agent { + return { + ...agent, + ...(update.status !== undefined && { status: update.status }), + ...(update.currentTask !== undefined && { currentTask: update.currentTask }), + ...(update.lastSeenAt && { lastSeenAt: update.lastSeenAt }), + ...(update.capabilities && { + capabilities: { ...agent.capabilities, ...update.capabilities }, + }), + }; +} + +/** + * Check if an agent is active (connected and seen recently) + */ +export function isAgentActive(agent: Agent, timeoutMs: number = 30000): boolean { + if (agent.status !== AgentStatus.CONNECTED) { + return false; + } + + const timeSinceLastSeen = Date.now() - agent.lastSeenAt.getTime(); + return timeSinceLastSeen < timeoutMs; +} + +/** + * Check if an agent is available for task assignment + */ +export function isAgentAvailable(agent: Agent): boolean { + return ( + agent.status === AgentStatus.CONNECTED && + (agent.currentTask === null || agent.currentTask === undefined) + ); +} + +/** + * Get agent display name + */ +export function getAgentDisplayName(agent: Agent): string { + return `${agent.role} (${agent.platform}/${agent.environment})`; +} diff --git a/packages/mcp-server/src/agents/AgentRegistry.ts b/packages/mcp-server/src/agents/AgentRegistry.ts new file mode 100644 index 0000000..4beb48f --- /dev/null +++ b/packages/mcp-server/src/agents/AgentRegistry.ts @@ -0,0 +1,342 @@ +/** + * AgentRegistry - Track connected agents, add/remove, get by ID/role + * Maintains a registry of all agents participating in the coordination system + */ + +import type { Agent, AgentStatus, AgentQuery, AgentUpdate } from './Agent.js'; +import { + matchesAgentQuery, + isAgentActive, + updateAgent, + getAgentDisplayName, +} from './Agent.js'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Agent event types + */ +export type AgentEventType = 'agent_added' | 'agent_updated' | 'agent_removed' | 'agent_timeout'; + +/** + * Agent event + */ +export interface AgentEvent { + type: AgentEventType; + agent: Agent; + timestamp: Date; +} + +/** + * Agent event handler + */ +export type AgentEventHandler = (event: AgentEvent) => void | Promise; + +/** + * Agent registry configuration + */ +export interface AgentRegistryConfig { + /** Logger */ + logger?: Logger; + + /** Timeout for agent inactivity (ms) */ + timeoutMs?: number; + + /** Whether to enable automatic timeout checking */ + enableTimeoutChecking?: boolean; +} + +/** + * AgentRegistry class + */ +export class AgentRegistry { + private agents: Map; // agentId β†’ Agent + private logger: Logger; + private eventHandlers: Set; + private timeoutMs: number; + private enableTimeoutChecking: boolean; + private timeoutCheckInterval?: NodeJS.Timeout; + + constructor(config: AgentRegistryConfig = {}) { + this.agents = new Map(); + this.logger = config.logger || createLogger(); + this.eventHandlers = new Set(); + this.timeoutMs = config.timeoutMs || 30000; // 30 seconds default + this.enableTimeoutChecking = config.enableTimeoutChecking ?? true; + + if (this.enableTimeoutChecking) { + this.startTimeoutChecking(); + } + } + + /** + * Add agent to registry + */ + async add(agent: Agent): Promise { + if (this.agents.has(agent.id)) { + this.logger.warn('Agent already registered', { agentId: agent.id }); + return; + } + + this.agents.set(agent.id, agent); + + await this.notifyHandlers({ + type: 'agent_added', + agent, + timestamp: new Date(), + }); + + this.logger.info('Agent added to registry', { + agentId: agent.id, + role: agent.role, + displayName: getAgentDisplayName(agent), + }); + } + + /** + * Update agent in registry + */ + async update(agentId: string, update: AgentUpdate): Promise { + const agent = this.agents.get(agentId); + if (!agent) { + this.logger.warn('Agent not found for update', { agentId }); + return undefined; + } + + const updatedAgent = updateAgent(agent, update); + this.agents.set(agentId, updatedAgent); + + await this.notifyHandlers({ + type: 'agent_updated', + agent: updatedAgent, + timestamp: new Date(), + }); + + this.logger.debug('Agent updated', { agentId, update }); + + return updatedAgent; + } + + /** + * Remove agent from registry + */ + async remove(agentId: string): Promise { + const agent = this.agents.get(agentId); + if (!agent) { + return false; + } + + this.agents.delete(agentId); + + await this.notifyHandlers({ + type: 'agent_removed', + agent, + timestamp: new Date(), + }); + + this.logger.info('Agent removed from registry', { + agentId, + role: agent.role, + }); + + return true; + } + + /** + * Get agent by ID + */ + getById(agentId: string): Agent | undefined { + return this.agents.get(agentId); + } + + /** + * Get agents by role + */ + getByRole(role: string): Agent[] { + return Array.from(this.agents.values()).filter( + (agent) => agent.role === role + ); + } + + /** + * Get agents by status + */ + getByStatus(status: AgentStatus): Agent[] { + return Array.from(this.agents.values()).filter( + (agent) => agent.status === status + ); + } + + /** + * Find agents matching query + */ + find(query: AgentQuery): Agent[] { + return Array.from(this.agents.values()).filter((agent) => + matchesAgentQuery(agent, query) + ); + } + + /** + * Get all agents + */ + getAll(): Agent[] { + return Array.from(this.agents.values()); + } + + /** + * Get active agents (connected and seen recently) + */ + getActive(): Agent[] { + return Array.from(this.agents.values()).filter((agent) => + isAgentActive(agent, this.timeoutMs) + ); + } + + /** + * Get agent count + */ + count(): number { + return this.agents.size; + } + + /** + * Check if agent exists + */ + has(agentId: string): boolean { + return this.agents.has(agentId); + } + + /** + * Clear all agents + */ + clear(): void { + this.agents.clear(); + this.logger.info('Agent registry cleared'); + } + + /** + * Update agent's last seen timestamp + */ + async heartbeat(agentId: string): Promise { + await this.update(agentId, { lastSeenAt: new Date() }); + } + + /** + * Start timeout checking + */ + private startTimeoutChecking(): void { + this.timeoutCheckInterval = setInterval(() => { + this.checkTimeouts().catch((error) => { + this.logger.error('Error checking timeouts', { + error: error instanceof Error ? error : new Error(String(error)), + }); + }); + }, this.timeoutMs / 2); // Check at half the timeout interval + } + + /** + * Check for timed-out agents + */ + private async checkTimeouts(): Promise { + const now = Date.now(); + const timedOutAgents: Agent[] = []; + + for (const agent of this.agents.values()) { + const timeSinceLastSeen = now - agent.lastSeenAt.getTime(); + if (timeSinceLastSeen > this.timeoutMs && agent.status !== 'disconnected') { + timedOutAgents.push(agent); + } + } + + // Handle timed-out agents + for (const agent of timedOutAgents) { + this.logger.warn('Agent timed out', { + agentId: agent.id, + role: agent.role, + lastSeenAt: agent.lastSeenAt, + }); + + // Update status to disconnected + await this.update(agent.id, { status: 'disconnected' }); + + // Notify handlers + await this.notifyHandlers({ + type: 'agent_timeout', + agent, + timestamp: new Date(), + }); + } + } + + /** + * Stop timeout checking + */ + stopTimeoutChecking(): void { + if (this.timeoutCheckInterval) { + clearInterval(this.timeoutCheckInterval); + this.timeoutCheckInterval = undefined; + } + } + + /** + * Register event handler + */ + onEvent(handler: AgentEventHandler): () => void { + this.eventHandlers.add(handler); + return () => this.eventHandlers.delete(handler); + } + + /** + * Notify event handlers + */ + private async notifyHandlers(event: AgentEvent): Promise { + const promises = Array.from(this.eventHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in agent event handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Get registry statistics + */ + getStats(): { + total: number; + active: number; + connected: number; + disconnected: number; + byRole: Record; + } { + const all = this.getAll(); + const active = this.getActive(); + const connected = this.getByStatus('connected'); + const disconnected = this.getByStatus('disconnected'); + + // Count by role + const byRole: Record = {}; + for (const agent of all) { + byRole[agent.role] = (byRole[agent.role] || 0) + 1; + } + + return { + total: all.length, + active: active.length, + connected: connected.length, + disconnected: disconnected.length, + byRole, + }; + } + + /** + * Cleanup (stop timeout checking) + */ + destroy(): void { + this.stopTimeoutChecking(); + } +} diff --git a/packages/mcp-server/src/agents/Capability.ts b/packages/mcp-server/src/agents/Capability.ts new file mode 100644 index 0000000..a8eee7e --- /dev/null +++ b/packages/mcp-server/src/agents/Capability.ts @@ -0,0 +1,220 @@ +/** + * Capability - Agent capability registration and discovery + * Based on specs/001-multi-agent-coordination/contracts/capability-schema.json + */ + +/** + * Resource limits for agent capabilities + */ +export interface ResourceLimits { + /** Maximum API calls per hour */ + apiQuotaPerHour?: number; + + /** Maximum number of simultaneous tasks (1-10) */ + maxConcurrentTasks?: number; + + /** Maximum requests per minute */ + rateLimitPerMinute?: number; + + /** Memory constraint in megabytes */ + memoryLimitMB?: number; +} + +/** + * Operating system platform + */ +export type Platform = 'Linux' | 'macOS' | 'Windows'; + +/** + * Agent capability registration + */ +export interface Capability { + /** Unique identifier for the agent */ + agentId: string; + + /** Agent role (extensible: developer, tester, architect, or custom) */ + roleType: string; + + /** Operating system platform */ + platform: Platform; + + /** Execution environment */ + environmentType?: string; + + /** Available commands, CLIs, or APIs the agent can use */ + tools: string[]; + + /** Programming languages the agent can work with */ + languages?: string[]; + + /** External APIs the agent has access to */ + apiAccess?: string[]; + + /** Resource constraints and quotas for the agent */ + resourceLimits?: ResourceLimits; + + /** Custom capability metadata for specialized agent types */ + customMetadata?: Record; +} + +/** + * Capability query filter + */ +export interface CapabilityQuery { + /** Filter by role type */ + roleType?: string; + + /** Filter by platform */ + platform?: Platform; + + /** Filter by environment type */ + environmentType?: string; + + /** Filter by required tools (must have all) */ + requiredTools?: string[]; + + /** Filter by required languages (must have all) */ + requiredLanguages?: string[]; + + /** Filter by required API access (must have all) */ + requiredApiAccess?: string[]; + + /** Filter by minimum resource limits */ + minResourceLimits?: Partial; +} + +/** + * Validate capability object + */ +export function validateCapability(capability: unknown): capability is Capability { + if (typeof capability !== 'object' || capability === null) { + return false; + } + + const cap = capability as Partial; + + return ( + typeof cap.agentId === 'string' && + typeof cap.roleType === 'string' && + cap.roleType.length > 0 && + cap.roleType.length <= 50 && + typeof cap.platform === 'string' && + ['Linux', 'macOS', 'Windows'].includes(cap.platform) && + Array.isArray(cap.tools) && + cap.tools.length > 0 && + cap.tools.every((tool) => typeof tool === 'string') + ); +} + +/** + * Check if a capability matches a query + */ +export function matchesQuery( + capability: Capability, + query: CapabilityQuery +): boolean { + // Check role type + if (query.roleType && capability.roleType !== query.roleType) { + return false; + } + + // Check platform + if (query.platform && capability.platform !== query.platform) { + return false; + } + + // Check environment type + if ( + query.environmentType && + capability.environmentType !== query.environmentType + ) { + return false; + } + + // Check required tools + if (query.requiredTools) { + const hasAllTools = query.requiredTools.every((tool) => + capability.tools.includes(tool) + ); + if (!hasAllTools) { + return false; + } + } + + // Check required languages + if (query.requiredLanguages && capability.languages) { + const hasAllLanguages = query.requiredLanguages.every((lang) => + capability.languages!.includes(lang) + ); + if (!hasAllLanguages) { + return false; + } + } + + // Check required API access + if (query.requiredApiAccess && capability.apiAccess) { + const hasAllApis = query.requiredApiAccess.every((api) => + capability.apiAccess!.includes(api) + ); + if (!hasAllApis) { + return false; + } + } + + // Check minimum resource limits + if (query.minResourceLimits && capability.resourceLimits) { + if ( + query.minResourceLimits.apiQuotaPerHour && + (capability.resourceLimits.apiQuotaPerHour || 0) < + query.minResourceLimits.apiQuotaPerHour + ) { + return false; + } + + if ( + query.minResourceLimits.maxConcurrentTasks && + (capability.resourceLimits.maxConcurrentTasks || 1) < + query.minResourceLimits.maxConcurrentTasks + ) { + return false; + } + + if ( + query.minResourceLimits.rateLimitPerMinute && + (capability.resourceLimits.rateLimitPerMinute || 0) < + query.minResourceLimits.rateLimitPerMinute + ) { + return false; + } + + if ( + query.minResourceLimits.memoryLimitMB && + (capability.resourceLimits.memoryLimitMB || 0) < + query.minResourceLimits.memoryLimitMB + ) { + return false; + } + } + + return true; +} + +/** + * Create a default capability object + */ +export function createDefaultCapability( + agentId: string, + roleType: string, + platform: Platform, + tools: string[] +): Capability { + return { + agentId, + roleType, + platform, + tools, + resourceLimits: { + maxConcurrentTasks: 1, + }, + }; +} diff --git a/packages/mcp-server/src/agents/RoleManager.ts b/packages/mcp-server/src/agents/RoleManager.ts new file mode 100644 index 0000000..5359e6a --- /dev/null +++ b/packages/mcp-server/src/agents/RoleManager.ts @@ -0,0 +1,411 @@ +/** + * RoleManager - Extensible role definitions and validation for custom roles + * Manages predefined and custom agent role types + */ + +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Role definition + */ +export interface RoleDefinition { + /** Role name (unique identifier) */ + name: string; + + /** Human-readable description */ + description: string; + + /** Predefined or custom */ + type: 'predefined' | 'custom'; + + /** Suggested capabilities */ + suggestedCapabilities?: { + tools?: string[]; + languages?: string[]; + apiAccess?: string[]; + }; + + /** Metadata */ + metadata?: Record; + + /** When role was registered */ + registeredAt: Date; +} + +/** + * Role validation result + */ +export interface RoleValidation { + valid: boolean; + errors?: string[]; +} + +/** + * Role manager configuration + */ +export interface RoleManagerConfig { + /** Logger */ + logger?: Logger; + + /** Whether to allow custom roles */ + allowCustomRoles?: boolean; + + /** Maximum role name length */ + maxRoleNameLength?: number; +} + +/** + * RoleManager class + */ +export class RoleManager { + private roles: Map; + private logger: Logger; + private allowCustomRoles: boolean; + private maxRoleNameLength: number; + + constructor(config: RoleManagerConfig = {}) { + this.roles = new Map(); + this.logger = config.logger || createLogger(); + this.allowCustomRoles = config.allowCustomRoles ?? true; + this.maxRoleNameLength = config.maxRoleNameLength || 50; + + // Register predefined roles + this.registerPredefinedRoles(); + } + + /** + * Register predefined roles + */ + private registerPredefinedRoles(): void { + const predefinedRoles: Omit[] = [ + { + name: 'developer', + description: 'Software developer - writes code, implements features', + type: 'predefined', + suggestedCapabilities: { + tools: ['git', 'npm', 'docker'], + languages: ['TypeScript', 'JavaScript', 'Python'], + }, + }, + { + name: 'tester', + description: 'Quality assurance tester - writes and runs tests', + type: 'predefined', + suggestedCapabilities: { + tools: ['jest', 'pytest', 'selenium', 'playwright'], + languages: ['TypeScript', 'JavaScript', 'Python'], + }, + }, + { + name: 'architect', + description: 'Software architect - designs system architecture', + type: 'predefined', + suggestedCapabilities: { + tools: ['draw.io', 'plantuml'], + }, + }, + { + name: 'frontend', + description: 'Frontend developer - UI/UX implementation', + type: 'predefined', + suggestedCapabilities: { + tools: ['npm', 'webpack', 'vite'], + languages: ['TypeScript', 'JavaScript', 'HTML', 'CSS'], + }, + }, + { + name: 'backend', + description: 'Backend developer - server-side implementation', + type: 'predefined', + suggestedCapabilities: { + tools: ['npm', 'docker', 'database-cli'], + languages: ['TypeScript', 'JavaScript', 'Python', 'Go'], + }, + }, + { + name: 'infrastructure', + description: 'Infrastructure engineer - DevOps, deployment, monitoring', + type: 'predefined', + suggestedCapabilities: { + tools: ['docker', 'kubernetes', 'terraform', 'aws-cli', 'gcloud'], + }, + }, + { + name: 'security-auditor', + description: 'Security auditor - security analysis and vulnerability testing', + type: 'predefined', + suggestedCapabilities: { + tools: ['nmap', 'burp-suite', 'owasp-zap'], + }, + }, + { + name: 'documentation-writer', + description: 'Technical writer - creates and maintains documentation', + type: 'predefined', + suggestedCapabilities: { + tools: ['markdown', 'docusaurus', 'sphinx'], + }, + }, + ]; + + for (const role of predefinedRoles) { + this.roles.set(role.name, { + ...role, + registeredAt: new Date(), + }); + } + + this.logger.info('Predefined roles registered', { + count: predefinedRoles.length, + }); + } + + /** + * Register a custom role + */ + registerCustomRole( + name: string, + description: string, + suggestedCapabilities?: RoleDefinition['suggestedCapabilities'], + metadata?: Record + ): RoleDefinition { + // Validate role name + const validation = this.validateRoleName(name); + if (!validation.valid) { + throw new Error(`Invalid role name: ${validation.errors?.join(', ')}`); + } + + // Check if custom roles are allowed + if (!this.allowCustomRoles) { + throw new Error('Custom roles are not allowed'); + } + + // Check if role already exists + if (this.roles.has(name)) { + throw new Error(`Role already exists: ${name}`); + } + + const role: RoleDefinition = { + name, + description, + type: 'custom', + suggestedCapabilities, + metadata, + registeredAt: new Date(), + }; + + this.roles.set(name, role); + + this.logger.info('Custom role registered', { name, description }); + + return role; + } + + /** + * Get role definition + */ + getRole(name: string): RoleDefinition | undefined { + return this.roles.get(name); + } + + /** + * Check if role exists + */ + hasRole(name: string): boolean { + return this.roles.has(name); + } + + /** + * Get all roles + */ + getAllRoles(): RoleDefinition[] { + return Array.from(this.roles.values()); + } + + /** + * Get predefined roles + */ + getPredefinedRoles(): RoleDefinition[] { + return Array.from(this.roles.values()).filter( + (role) => role.type === 'predefined' + ); + } + + /** + * Get custom roles + */ + getCustomRoles(): RoleDefinition[] { + return Array.from(this.roles.values()).filter( + (role) => role.type === 'custom' + ); + } + + /** + * Validate role name + */ + validateRoleName(name: string): RoleValidation { + const errors: string[] = []; + + if (!name || name.trim().length === 0) { + errors.push('Role name cannot be empty'); + } + + if (name.length > this.maxRoleNameLength) { + errors.push(`Role name too long (max: ${this.maxRoleNameLength})`); + } + + if (!/^[a-z0-9-]+$/.test(name)) { + errors.push('Role name must contain only lowercase letters, numbers, and hyphens'); + } + + if (name.startsWith('-') || name.endsWith('-')) { + errors.push('Role name cannot start or end with a hyphen'); + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined, + }; + } + + /** + * Validate role (check if it exists or can be created) + */ + validateRole(name: string): RoleValidation { + // Check if role exists + if (this.hasRole(name)) { + return { valid: true }; + } + + // Check if role name is valid for custom roles + if (!this.allowCustomRoles) { + return { + valid: false, + errors: ['Custom roles are not allowed, and role is not predefined'], + }; + } + + return this.validateRoleName(name); + } + + /** + * Remove custom role + */ + removeCustomRole(name: string): boolean { + const role = this.roles.get(name); + if (!role) { + return false; + } + + if (role.type === 'predefined') { + throw new Error('Cannot remove predefined role'); + } + + this.roles.delete(name); + this.logger.info('Custom role removed', { name }); + return true; + } + + /** + * Update role description + */ + updateRole( + name: string, + updates: { + description?: string; + suggestedCapabilities?: RoleDefinition['suggestedCapabilities']; + metadata?: Record; + } + ): RoleDefinition | undefined { + const role = this.roles.get(name); + if (!role) { + return undefined; + } + + if (role.type === 'predefined') { + throw new Error('Cannot update predefined role'); + } + + const updatedRole: RoleDefinition = { + ...role, + ...(updates.description && { description: updates.description }), + ...(updates.suggestedCapabilities && { + suggestedCapabilities: updates.suggestedCapabilities, + }), + ...(updates.metadata && { metadata: updates.metadata }), + }; + + this.roles.set(name, updatedRole); + this.logger.info('Role updated', { name }); + + return updatedRole; + } + + /** + * Get role suggestions based on capabilities + */ + suggestRoles(capabilities: { + tools?: string[]; + languages?: string[]; + }): RoleDefinition[] { + const suggestions: Array<{ role: RoleDefinition; score: number }> = []; + + for (const role of this.roles.values()) { + if (!role.suggestedCapabilities) { + continue; + } + + let score = 0; + + // Match tools + if (capabilities.tools && role.suggestedCapabilities.tools) { + const matchingTools = capabilities.tools.filter((tool) => + role.suggestedCapabilities.tools?.includes(tool) + ); + score += matchingTools.length; + } + + // Match languages + if (capabilities.languages && role.suggestedCapabilities.languages) { + const matchingLanguages = capabilities.languages.filter((lang) => + role.suggestedCapabilities.languages?.includes(lang) + ); + score += matchingLanguages.length; + } + + if (score > 0) { + suggestions.push({ role, score }); + } + } + + // Sort by score (highest first) + suggestions.sort((a, b) => b.score - a.score); + + return suggestions.map((s) => s.role); + } + + /** + * Get statistics + */ + getStats(): { + total: number; + predefined: number; + custom: number; + } { + const all = this.getAllRoles(); + const predefined = this.getPredefinedRoles(); + const custom = this.getCustomRoles(); + + return { + total: all.length, + predefined: predefined.length, + custom: custom.length, + }; + } +} + +/** + * Singleton role manager instance + */ +export const roleManager = new RoleManager(); diff --git a/packages/mcp-server/src/channels/base/Channel.ts b/packages/mcp-server/src/channels/base/Channel.ts new file mode 100644 index 0000000..a3c6079 --- /dev/null +++ b/packages/mcp-server/src/channels/base/Channel.ts @@ -0,0 +1,187 @@ +/** + * Channel - Base interface for all communication channels + * + * Defines the contract for Discord, SignalR, Redis, and Relay channel implementations + */ + +import { Message } from '../../protocol/Message.js'; + +/** + * Channel connection status + */ +export enum ConnectionStatus { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + RECONNECTING = 'reconnecting', + FAILED = 'failed', +} + +/** + * Channel configuration + */ +export interface ChannelConfig { + /** Channel type identifier */ + type: 'discord' | 'signalr' | 'redis' | 'relay'; + + /** Authentication token */ + token: string; + + /** Channel-specific connection parameters */ + connectionParams: Record; + + /** Retry configuration */ + retry?: { + enabled: boolean; + maxAttempts: number; + initialDelayMs: number; + maxDelayMs: number; + }; + + /** Heartbeat configuration */ + heartbeat?: { + enabled: boolean; + intervalMs: number; + timeoutMs: number; + }; +} + +/** + * Channel statistics + */ +export interface ChannelStats { + messagesSent: number; + messagesReceived: number; + messagesFailed: number; + bytesTransferred: number; + uptime: number; + lastHeartbeat?: Date; + lastError?: string; +} + +/** + * Message handler callback + */ +export type MessageHandler = (message: Message) => void | Promise; + +/** + * Error handler callback + */ +export type ErrorHandler = (error: Error) => void | Promise; + +/** + * Connection state change callback + */ +export type ConnectionStateHandler = ( + status: ConnectionStatus, + previousStatus: ConnectionStatus +) => void | Promise; + +/** + * Base Channel interface + * All channel implementations must implement this interface + */ +export interface Channel { + /** + * Get channel unique identifier + */ + readonly id: string; + + /** + * Get channel type + */ + readonly type: string; + + /** + * Get current connection status + */ + readonly status: ConnectionStatus; + + /** + * Connect to the channel + * @throws Error if connection fails + */ + connect(): Promise; + + /** + * Disconnect from the channel + */ + disconnect(): Promise; + + /** + * Send a message through the channel + * @param message - Message to send + * @returns Promise that resolves when message is sent + * @throws Error if send fails + */ + sendMessage(message: Message): Promise; + + /** + * Register a message handler + * @param handler - Function to call when a message is received + * @returns Function to unregister the handler + */ + onMessage(handler: MessageHandler): () => void; + + /** + * Register an error handler + * @param handler - Function to call when an error occurs + * @returns Function to unregister the handler + */ + onError(handler: ErrorHandler): () => void; + + /** + * Register a connection state change handler + * @param handler - Function to call when connection state changes + * @returns Function to unregister the handler + */ + onConnectionStateChange(handler: ConnectionStateHandler): () => void; + + /** + * Get channel statistics + */ + getStats(): ChannelStats; + + /** + * Check if channel is connected + */ + isConnected(): boolean; + + /** + * Retrieve message history (if supported by channel) + * @param limit - Maximum number of messages to retrieve + * @param before - Retrieve messages before this timestamp + * @returns Array of messages (empty if not supported) + */ + getHistory(limit?: number, before?: Date): Promise; + + /** + * Ping the channel to check connectivity + * @returns Round-trip time in milliseconds + * @throws Error if ping fails + */ + ping(): Promise; +} + +/** + * Type guard to check if an object implements the Channel interface + */ +export function isChannel(obj: unknown): obj is Channel { + if (typeof obj !== 'object' || obj === null) { + return false; + } + + const channel = obj as Partial; + + return ( + typeof channel.id === 'string' && + typeof channel.type === 'string' && + typeof channel.status === 'string' && + typeof channel.connect === 'function' && + typeof channel.disconnect === 'function' && + typeof channel.sendMessage === 'function' && + typeof channel.onMessage === 'function' && + typeof channel.onError === 'function' && + typeof channel.isConnected === 'function' + ); +} diff --git a/packages/mcp-server/src/channels/base/ChannelAdapter.ts b/packages/mcp-server/src/channels/base/ChannelAdapter.ts new file mode 100644 index 0000000..239fde4 --- /dev/null +++ b/packages/mcp-server/src/channels/base/ChannelAdapter.ts @@ -0,0 +1,459 @@ +/** + * ChannelAdapter - Base class for channel implementations + * + * Provides common functionality: + * - Reconnection logic with exponential backoff + * - Error handling and recovery + * - Message handler management + * - Statistics tracking + * - Heartbeat mechanism + */ + +import { v4 as uuidv4 } from 'uuid'; +import { + Channel, + ChannelConfig, + ChannelStats, + ConnectionStatus, + MessageHandler, + ErrorHandler, + ConnectionStateHandler, +} from './Channel.js'; +import { Message } from '../../protocol/Message.js'; + +/** + * Abstract base class for channel implementations + */ +export abstract class ChannelAdapter implements Channel { + public readonly id: string; + public readonly type: string; + protected config: ChannelConfig; + protected _status: ConnectionStatus; + protected stats: ChannelStats; + protected messageHandlers: Set; + protected errorHandlers: Set; + protected stateChangeHandlers: Set; + protected reconnectAttempts: number; + protected reconnectTimer?: NodeJS.Timeout; + protected heartbeatTimer?: NodeJS.Timeout; + protected connectionStartTime?: Date; + + constructor(config: ChannelConfig) { + this.id = uuidv4(); + this.type = config.type; + this.config = config; + this._status = ConnectionStatus.DISCONNECTED; + this.reconnectAttempts = 0; + this.messageHandlers = new Set(); + this.errorHandlers = new Set(); + this.stateChangeHandlers = new Set(); + this.stats = { + messagesSent: 0, + messagesReceived: 0, + messagesFailed: 0, + bytesTransferred: 0, + uptime: 0, + }; + + // Validate authentication token + this.validateToken(); + } + + /** + * Validate authentication token + */ + protected validateToken(): void { + if (!this.config.token || this.config.token.length < 16) { + throw new Error('Invalid or missing authentication token (minimum 16 characters)'); + } + } + + /** + * Verify token matches expected value + */ + protected verifyToken(providedToken: string): boolean { + // Use timing-safe comparison to prevent timing attacks + if (!providedToken || !this.config.token) { + return false; + } + + // Convert to buffers for timing-safe comparison + const providedBuffer = Buffer.from(providedToken); + const expectedBuffer = Buffer.from(this.config.token); + + // Lengths must match + if (providedBuffer.length !== expectedBuffer.length) { + return false; + } + + // Timing-safe comparison + let result = 0; + for (let i = 0; i < providedBuffer.length; i++) { + result |= providedBuffer[i] ^ expectedBuffer[i]; + } + + return result === 0; + } + + /** + * Get authentication token for outbound connections + */ + protected getAuthToken(): string { + return this.config.token; + } + + /** + * Get current connection status + */ + get status(): ConnectionStatus { + return this._status; + } + + /** + * Set connection status and notify handlers + */ + protected setStatus(newStatus: ConnectionStatus): void { + const previousStatus = this._status; + if (previousStatus === newStatus) { + return; + } + + this._status = newStatus; + + // Update connection start time + if (newStatus === ConnectionStatus.CONNECTED) { + this.connectionStartTime = new Date(); + this.reconnectAttempts = 0; + } + + // Notify state change handlers + this.stateChangeHandlers.forEach((handler) => { + this.safeCall(handler, newStatus, previousStatus); + }); + } + + /** + * Connect to the channel (with retry logic) + */ + async connect(): Promise { + if (this.isConnected()) { + return; + } + + this.setStatus(ConnectionStatus.CONNECTING); + + try { + await this.doConnect(); + this.setStatus(ConnectionStatus.CONNECTED); + this.startHeartbeat(); + } catch (error) { + this.setStatus(ConnectionStatus.FAILED); + this.handleError( + error instanceof Error + ? error + : new Error(`Connection failed: ${String(error)}`) + ); + + // Attempt reconnection if configured + if (this.config.retry?.enabled) { + await this.scheduleReconnect(); + } else { + throw error; + } + } + } + + /** + * Disconnect from the channel + */ + async disconnect(): Promise { + this.stopHeartbeat(); + this.clearReconnectTimer(); + + if (this._status === ConnectionStatus.DISCONNECTED) { + return; + } + + try { + await this.doDisconnect(); + } finally { + this.setStatus(ConnectionStatus.DISCONNECTED); + this.connectionStartTime = undefined; + } + } + + /** + * Send a message through the channel + */ + async sendMessage(message: Message): Promise { + if (!this.isConnected()) { + throw new Error(`Cannot send message: channel is ${this._status}`); + } + + try { + // Route message based on recipient + await this.routeMessage(message); + this.stats.messagesSent++; + this.stats.bytesTransferred += this.estimateMessageSize(message); + } catch (error) { + this.stats.messagesFailed++; + this.handleError( + error instanceof Error + ? error + : new Error(`Failed to send message: ${String(error)}`) + ); + throw error; + } + } + + /** + * Route message (broadcast vs unicast) + */ + protected async routeMessage(message: Message): Promise { + if (this.isBroadcast(message)) { + // Broadcast message to all participants + await this.broadcastMessage(message); + } else { + // Unicast message to specific recipient + await this.unicastMessage(message); + } + } + + /** + * Check if message is a broadcast + */ + protected isBroadcast(message: Message): boolean { + return !message.recipientId || message.recipientId === null; + } + + /** + * Broadcast message to all participants + */ + protected async broadcastMessage(message: Message): Promise { + // Default implementation: send to channel (all subscribers receive it) + await this.doSendMessage(message); + } + + /** + * Unicast message to specific recipient + */ + protected async unicastMessage(message: Message): Promise { + // Default implementation: still send to channel + // Subclasses can override for direct messaging if supported + await this.doSendMessage(message); + } + + /** + * Register a message handler + */ + onMessage(handler: MessageHandler): () => void { + this.messageHandlers.add(handler); + return () => this.messageHandlers.delete(handler); + } + + /** + * Register an error handler + */ + onError(handler: ErrorHandler): () => void { + this.errorHandlers.add(handler); + return () => this.errorHandlers.delete(handler); + } + + /** + * Register a connection state change handler + */ + onConnectionStateChange(handler: ConnectionStateHandler): () => void { + this.stateChangeHandlers.add(handler); + return () => this.stateChangeHandlers.delete(handler); + } + + /** + * Get channel statistics + */ + getStats(): ChannelStats { + return { + ...this.stats, + uptime: this.connectionStartTime + ? Date.now() - this.connectionStartTime.getTime() + : 0, + }; + } + + /** + * Check if channel is connected + */ + isConnected(): boolean { + return this._status === ConnectionStatus.CONNECTED; + } + + /** + * Retrieve message history (default: not supported) + */ + async getHistory(_limit?: number, _before?: Date): Promise { + return []; + } + + /** + * Ping the channel (default implementation) + */ + async ping(): Promise { + const start = Date.now(); + await this.doPing(); + return Date.now() - start; + } + + /** + * Abstract methods to be implemented by subclasses + */ + protected abstract doConnect(): Promise; + protected abstract doDisconnect(): Promise; + protected abstract doSendMessage(message: Message): Promise; + protected abstract doPing(): Promise; + + /** + * Handle received message + */ + protected handleMessage(message: Message): void { + this.stats.messagesReceived++; + this.stats.bytesTransferred += this.estimateMessageSize(message); + + this.messageHandlers.forEach((handler) => { + this.safeCall(handler, message); + }); + } + + /** + * Handle error + */ + protected handleError(error: Error): void { + this.stats.lastError = error.message; + + this.errorHandlers.forEach((handler) => { + this.safeCall(handler, error); + }); + } + + /** + * Schedule reconnection with exponential backoff + */ + protected async scheduleReconnect(): Promise { + if (!this.config.retry?.enabled) { + return; + } + + this.clearReconnectTimer(); + + if (this.reconnectAttempts >= (this.config.retry.maxAttempts || 5)) { + this.handleError( + new Error('Max reconnection attempts reached, giving up') + ); + return; + } + + this.reconnectAttempts++; + const delay = this.calculateBackoff(); + + this.setStatus(ConnectionStatus.RECONNECTING); + + this.reconnectTimer = setTimeout(async () => { + try { + await this.connect(); + } catch (error) { + // connect() already handles retry + } + }, delay); + } + + /** + * Calculate exponential backoff delay with jitter + */ + protected calculateBackoff(): number { + const { initialDelayMs = 1000, maxDelayMs = 60000 } = this.config.retry || {}; + const exponentialDelay = initialDelayMs * Math.pow(2, this.reconnectAttempts - 1); + const jitter = Math.random() * 0.3 * exponentialDelay; // 30% jitter + return Math.min(exponentialDelay + jitter, maxDelayMs); + } + + /** + * Clear reconnect timer + */ + protected clearReconnectTimer(): void { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = undefined; + } + } + + /** + * Start heartbeat mechanism + */ + protected startHeartbeat(): void { + if (!this.config.heartbeat?.enabled) { + return; + } + + this.stopHeartbeat(); + + this.heartbeatTimer = setInterval(() => { + this.sendHeartbeat().catch((error) => { + this.handleError( + error instanceof Error + ? error + : new Error(`Heartbeat failed: ${String(error)}`) + ); + }); + }, this.config.heartbeat.intervalMs || 15000); + } + + /** + * Stop heartbeat mechanism + */ + protected stopHeartbeat(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = undefined; + } + } + + /** + * Send heartbeat (can be overridden by subclasses) + */ + protected async sendHeartbeat(): Promise { + try { + await this.ping(); + this.stats.lastHeartbeat = new Date(); + } catch (error) { + // Heartbeat failed, trigger reconnection if needed + if (this.config.retry?.enabled) { + await this.scheduleReconnect(); + } + throw error; + } + } + + /** + * Estimate message size in bytes + */ + protected estimateMessageSize(message: Message): number { + return JSON.stringify(message).length; + } + + /** + * Safely call a handler with error protection + */ + protected safeCall( + handler: (...args: T) => void | Promise, + ...args: T + ): void { + try { + const result = handler(...args); + if (result instanceof Promise) { + result.catch((error) => { + console.error('Handler error:', error); + }); + } + } catch (error) { + console.error('Handler error:', error); + } + } +} diff --git a/packages/mcp-server/src/channels/base/ChannelFactory.ts b/packages/mcp-server/src/channels/base/ChannelFactory.ts new file mode 100644 index 0000000..72bfb9a --- /dev/null +++ b/packages/mcp-server/src/channels/base/ChannelFactory.ts @@ -0,0 +1,194 @@ +/** + * ChannelFactory - Factory pattern for creating channel instances + * + * Creates appropriate channel implementation based on configuration + */ + +import { Channel, ChannelConfig } from './Channel.js'; + +/** + * Channel constructor type + */ +export type ChannelConstructor = new (config: ChannelConfig) => Channel; + +/** + * Factory for creating channel instances + */ +export class ChannelFactory { + /** + * Registry of channel constructors by type + */ + private static registry: Map = new Map(); + + /** + * Register a channel implementation + * @param type - Channel type identifier + * @param constructor - Channel class constructor + */ + static register(type: string, constructor: ChannelConstructor): void { + if (this.registry.has(type)) { + throw new Error(`Channel type "${type}" is already registered`); + } + this.registry.set(type, constructor); + } + + /** + * Unregister a channel implementation + * @param type - Channel type identifier + */ + static unregister(type: string): void { + this.registry.delete(type); + } + + /** + * Check if a channel type is registered + * @param type - Channel type identifier + */ + static isRegistered(type: string): boolean { + return this.registry.has(type); + } + + /** + * Get all registered channel types + */ + static getRegisteredTypes(): string[] { + return Array.from(this.registry.keys()); + } + + /** + * Create a channel instance + * @param config - Channel configuration + * @returns Channel instance + * @throws Error if channel type is not registered + */ + static create(config: ChannelConfig): Channel { + const constructor = this.registry.get(config.type); + + if (!constructor) { + throw new Error( + `Unknown channel type: "${config.type}". ` + + `Available types: ${this.getRegisteredTypes().join(', ')}` + ); + } + + try { + return new constructor(config); + } catch (error) { + throw new Error( + `Failed to create channel of type "${config.type}": ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + /** + * Create multiple channel instances from an array of configurations + * @param configs - Array of channel configurations + * @returns Array of channel instances + */ + static createMany(configs: ChannelConfig[]): Channel[] { + return configs.map((config) => this.create(config)); + } + + /** + * Validate channel configuration + * @param config - Channel configuration to validate + * @throws Error if configuration is invalid + */ + static validateConfig(config: ChannelConfig): void { + if (!config.type) { + throw new Error('Channel configuration must include a type'); + } + + if (!config.token) { + throw new Error('Channel configuration must include an authentication token'); + } + + if (!config.connectionParams || typeof config.connectionParams !== 'object') { + throw new Error('Channel configuration must include connectionParams object'); + } + + if (!this.isRegistered(config.type)) { + throw new Error( + `Channel type "${config.type}" is not registered. ` + + `Available types: ${this.getRegisteredTypes().join(', ')}` + ); + } + } + + /** + * Create a channel with validation + * @param config - Channel configuration + * @returns Channel instance + * @throws Error if configuration is invalid or creation fails + */ + static createSafe(config: ChannelConfig): Channel { + this.validateConfig(config); + return this.create(config); + } + + /** + * Clear all registered channel types (useful for testing) + */ + static clear(): void { + this.registry.clear(); + } + + /** + * Get default retry configuration + */ + static getDefaultRetryConfig() { + return { + enabled: true, + maxAttempts: 5, + initialDelayMs: 1000, + maxDelayMs: 60000, + }; + } + + /** + * Get default heartbeat configuration + */ + static getDefaultHeartbeatConfig() { + return { + enabled: true, + intervalMs: 15000, + timeoutMs: 30000, + }; + } + + /** + * Create a channel configuration with defaults + * @param type - Channel type + * @param token - Authentication token + * @param connectionParams - Connection parameters + * @param overrides - Optional configuration overrides + */ + static createConfig( + type: 'discord' | 'signalr' | 'redis' | 'relay', + token: string, + connectionParams: Record, + overrides?: Partial + ): ChannelConfig { + return { + type, + token, + connectionParams, + retry: overrides?.retry ?? this.getDefaultRetryConfig(), + heartbeat: overrides?.heartbeat ?? this.getDefaultHeartbeatConfig(), + ...overrides, + }; + } +} + +/** + * Decorator for auto-registering channel implementations + * Usage: @RegisterChannel('discord') + */ +export function RegisterChannel(type: string) { + return function (constructor: T): T { + ChannelFactory.register(type, constructor); + return constructor; + }; +} diff --git a/packages/mcp-server/src/channels/discord/DiscordChannel.ts b/packages/mcp-server/src/channels/discord/DiscordChannel.ts new file mode 100644 index 0000000..d47f8d7 --- /dev/null +++ b/packages/mcp-server/src/channels/discord/DiscordChannel.ts @@ -0,0 +1,256 @@ +/** + * DiscordChannel - Discord.js implementation of Channel interface + * Provides Discord-based communication for agent coordination + */ + +import { Client, GatewayIntentBits, TextChannel, Message as DiscordMessage } from 'discord.js'; +import { ChannelAdapter } from '../base/ChannelAdapter.js'; +import type { ChannelConfig } from '../base/Channel.js'; +import type { Message } from '../../protocol/Message.js'; +import { MessageBuilder } from '../../protocol/MessageBuilder.js'; +import { validator } from '../../protocol/MessageValidator.js'; + +/** + * Discord-specific configuration + */ +export interface DiscordConfig { + guildId: string; + channelId: string; + botToken: string; +} + +/** + * DiscordChannel implementation + */ +export class DiscordChannel extends ChannelAdapter { + private client: Client; + private discordConfig: DiscordConfig; + private textChannel?: TextChannel; + + constructor(config: ChannelConfig) { + super(config); + + this.discordConfig = config.connectionParams as DiscordConfig; + + // Initialize Discord client + this.client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], + }); + + this.setupEventHandlers(); + } + + /** + * Setup Discord event handlers + */ + private setupEventHandlers(): void { + this.client.on('ready', () => { + this.logger.info('Discord client ready', { + username: this.client.user?.tag, + }); + }); + + this.client.on('messageCreate', (message) => { + this.handleDiscordMessage(message).catch((error) => { + this.handleError( + error instanceof Error ? error : new Error(String(error)) + ); + }); + }); + + this.client.on('error', (error) => { + this.handleError(error); + }); + } + + /** + * Handle incoming Discord message + */ + private async handleDiscordMessage(discordMessage: DiscordMessage): Promise { + // Ignore messages from bots (except our own for history) + if (discordMessage.author.bot && discordMessage.author.id !== this.client.user?.id) { + return; + } + + // Only process messages from our channel + if (discordMessage.channelId !== this.discordConfig.channelId) { + return; + } + + try { + // Parse message content as JSON + const content = discordMessage.content; + const parsedMessage = JSON.parse(content); + + // Validate message + const validationResult = validator.validateFull(parsedMessage); + if (!validationResult.valid) { + this.logger.warn('Invalid message received', { + errors: validator.getErrorSummary(validationResult), + }); + return; + } + + // Authenticate message: check if sender has valid token + // For Discord, we trust all messages in the channel since channel access is controlled + // by Discord's own authentication. The shared token is used for bot authentication. + if (!this.authenticateMessage(parsedMessage)) { + this.logger.warn('Message authentication failed', { + senderId: parsedMessage.senderId, + }); + return; + } + + // Handle the message + this.handleMessage(parsedMessage as Message); + } catch (error) { + // If parsing fails, it's likely not a protocol message + this.logger.debug('Failed to parse message as protocol message', { + content: discordMessage.content, + }); + } + } + + /** + * Authenticate incoming message + * For Discord, we rely on channel access control + */ + private authenticateMessage(message: any): boolean { + // Basic validation that message has required fields + // Discord channel access itself provides authentication + return true; + } + + /** + * Get authentication headers for Discord connection + */ + protected getAuthToken(): string { + return this.discordConfig.botToken; + } + + /** + * Connect to Discord + */ + protected async doConnect(): Promise { + // Login to Discord + await this.client.login(this.discordConfig.botToken); + + // Wait for client to be ready + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Discord connection timeout')); + }, 30000); + + this.client.once('ready', () => { + clearTimeout(timeout); + resolve(); + }); + }); + + // Get the text channel + const channel = await this.client.channels.fetch(this.discordConfig.channelId); + if (!channel || !channel.isTextBased()) { + throw new Error('Channel not found or is not a text channel'); + } + + this.textChannel = channel as TextChannel; + this.logger.info('Connected to Discord channel', { + guildId: this.discordConfig.guildId, + channelId: this.discordConfig.channelId, + }); + } + + /** + * Disconnect from Discord + */ + protected async doDisconnect(): Promise { + this.client.destroy(); + this.textChannel = undefined; + } + + /** + * Send message to Discord + */ + protected async doSendMessage(message: Message): Promise { + if (!this.textChannel) { + throw new Error('Not connected to Discord channel'); + } + + // Serialize message to JSON + const content = JSON.stringify(message); + + // Discord has a 2000 character limit + if (content.length > 2000) { + throw new Error(`Message too long: ${content.length} characters (max 2000)`); + } + + // Send message + await this.textChannel.send(content); + } + + /** + * Ping Discord + */ + protected async doPing(): Promise { + // Check if client is ready + if (!this.client.isReady()) { + throw new Error('Discord client not ready'); + } + + // Discord doesn't have a native ping, so we'll just check the websocket + const ping = this.client.ws.ping; + if (ping === -1) { + throw new Error('Discord websocket not connected'); + } + } + + /** + * Get message history from Discord + */ + async getHistory(limit: number = 50, before?: Date): Promise { + if (!this.textChannel) { + return []; + } + + try { + const options: any = { limit }; + if (before) { + // Discord uses snowflake IDs, we need to convert timestamp + // This is an approximation + options.before = String(before.getTime()); + } + + const messages = await this.textChannel.messages.fetch(options); + const parsedMessages: Message[] = []; + + for (const [_, discordMessage] of messages) { + if (discordMessage.author.bot) { + try { + const parsed = JSON.parse(discordMessage.content); + const validationResult = validator.validate(parsed); + if (validationResult.valid) { + parsedMessages.push(parsed as Message); + } + } catch { + // Skip invalid messages + } + } + } + + return parsedMessages; + } catch (error) { + this.logger.error('Failed to fetch message history', { + error: error instanceof Error ? error : new Error(String(error)), + }); + return []; + } + } +} + +// Register with factory +import { ChannelFactory } from '../base/ChannelFactory.js'; +ChannelFactory.register('discord', DiscordChannel); diff --git a/packages/mcp-server/src/channels/redis/RedisChannel.ts b/packages/mcp-server/src/channels/redis/RedisChannel.ts new file mode 100644 index 0000000..e3e73e9 --- /dev/null +++ b/packages/mcp-server/src/channels/redis/RedisChannel.ts @@ -0,0 +1,342 @@ +/** + * RedisChannel - Redis pub/sub implementation of Channel interface + * Provides Redis-based message queue for agent coordination + */ + +import Redis, { RedisOptions } from 'ioredis'; +import { ChannelAdapter } from '../base/ChannelAdapter.js'; +import type { ChannelConfig } from '../base/Channel.js'; +import type { Message } from '../../protocol/Message.js'; +import { validator } from '../../protocol/MessageValidator.js'; + +/** + * Redis-specific configuration + */ +export interface RedisConfig { + host: string; + port: number; + password?: string; + db?: number; + keyPrefix?: string; + tls?: boolean; +} + +/** + * RedisChannel implementation + */ +export class RedisChannel extends ChannelAdapter { + private publisher: Redis; + private subscriber: Redis; + private redisConfig: RedisConfig; + private channelName: string; + + constructor(config: ChannelConfig) { + super(config); + + this.redisConfig = config.connectionParams as RedisConfig; + this.channelName = `${this.redisConfig.keyPrefix || 'coorchat:'}channel`; + + // Create Redis options with authentication + const redisOptions: RedisOptions = { + host: this.redisConfig.host, + port: this.redisConfig.port, + password: this.redisConfig.password || this.config.token, + db: this.redisConfig.db || 0, + retryStrategy: (times) => { + // Exponential backoff + return Math.min(times * 1000, 30000); + }, + }; + + // Enable TLS for secure connections + if (this.redisConfig.tls) { + redisOptions.tls = { + // Reject unauthorized certificates in production + rejectUnauthorized: process.env.NODE_ENV === 'production', + // Additional TLS options can be configured here: + // - ca: Certificate Authority certificates + // - cert: Client certificate + // - key: Client private key + }; + this.logger.info('Redis TLS enabled', { + host: this.redisConfig.host, + rejectUnauthorized: redisOptions.tls.rejectUnauthorized, + }); + } else { + this.logger.warn('Redis TLS not enabled - connection may be insecure', { + host: this.redisConfig.host, + }); + } + + // Create separate connections for pub and sub + this.publisher = new Redis(redisOptions); + this.subscriber = new Redis(redisOptions); + + this.setupEventHandlers(); + } + + /** + * Setup Redis event handlers + */ + private setupEventHandlers(): void { + // Publisher events + this.publisher.on('error', (error) => { + this.logger.error('Redis publisher error', { error }); + this.handleError(error); + }); + + this.publisher.on('connect', () => { + this.logger.debug('Redis publisher connected'); + }); + + // Subscriber events + this.subscriber.on('error', (error) => { + this.logger.error('Redis subscriber error', { error }); + this.handleError(error); + }); + + this.subscriber.on('connect', () => { + this.logger.debug('Redis subscriber connected'); + }); + + this.subscriber.on('message', (channel, message) => { + if (channel === this.channelName) { + this.handleRedisMessage(message).catch((error) => { + this.handleError( + error instanceof Error ? error : new Error(String(error)) + ); + }); + } + }); + + this.subscriber.on('subscribe', (channel, count) => { + this.logger.info('Subscribed to Redis channel', { channel, count }); + }); + } + + /** + * Handle incoming Redis message + */ + private async handleRedisMessage(messageStr: string): Promise { + try { + // Parse message + const parsedMessage = JSON.parse(messageStr); + + // Verify authentication + if (!this.verifyAuthSignature(parsedMessage)) { + this.logger.warn('Message authentication failed', { + senderId: parsedMessage.senderId, + }); + return; + } + + // Validate message + const validationResult = validator.validateFull(parsedMessage); + if (!validationResult.valid) { + this.logger.warn('Invalid message received', { + errors: validator.getErrorSummary(validationResult), + }); + return; + } + + // Handle the message + this.handleMessage(parsedMessage as Message); + } catch (error) { + this.logger.error('Failed to parse Redis message', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Connect to Redis + */ + protected async doConnect(): Promise { + // Wait for both connections to be ready + await Promise.all([ + this.waitForConnection(this.publisher), + this.waitForConnection(this.subscriber), + ]); + + // Subscribe to channel + await this.subscriber.subscribe(this.channelName); + + this.logger.info('Connected to Redis', { + host: this.redisConfig.host, + port: this.redisConfig.port, + channel: this.channelName, + }); + } + + /** + * Wait for Redis connection + */ + private async waitForConnection(redis: Redis): Promise { + if (redis.status === 'ready') { + return; + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Redis connection timeout')); + }, 10000); + + redis.once('ready', () => { + clearTimeout(timeout); + resolve(); + }); + + redis.once('error', (error) => { + clearTimeout(timeout); + reject(error); + }); + }); + } + + /** + * Disconnect from Redis + */ + protected async doDisconnect(): Promise { + await this.subscriber.unsubscribe(this.channelName); + await Promise.all([this.publisher.quit(), this.subscriber.quit()]); + } + + /** + * Send message via Redis pub/sub + */ + protected async doSendMessage(message: Message): Promise { + // Add authentication metadata to message + const authenticatedMessage = { + ...message, + _auth: this.createAuthSignature(message), + }; + + // Serialize message to JSON + const messageStr = JSON.stringify(authenticatedMessage); + + // Publish to channel + const subscriberCount = await this.publisher.publish( + this.channelName, + messageStr + ); + + this.logger.debug('Message published to Redis', { + subscriberCount, + messageType: message.messageType, + }); + } + + /** + * Create authentication signature for message + */ + private createAuthSignature(message: Message): string { + const { createHmac } = require('crypto'); + const hmac = createHmac('sha256', this.config.token); + hmac.update(JSON.stringify({ + messageType: message.messageType, + senderId: message.senderId, + timestamp: message.timestamp, + })); + return hmac.digest('hex'); + } + + /** + * Verify message authentication + */ + private verifyAuthSignature(message: any): boolean { + if (!message._auth) { + return false; + } + + const providedSignature = message._auth; + delete message._auth; + + const expectedSignature = this.createAuthSignature(message); + return this.verifyToken(providedSignature) || providedSignature === expectedSignature; + } + + /** + * Ping Redis connection + */ + protected async doPing(): Promise { + const result = await this.publisher.ping(); + if (result !== 'PONG') { + throw new Error('Redis ping failed'); + } + } + + /** + * Get message history from Redis + * Note: Redis pub/sub doesn't persist messages by default + * This would require additional Redis Streams or List storage + */ + async getHistory(limit: number = 50, before?: Date): Promise { + try { + // Try to get messages from a Redis list (if implemented) + const historyKey = `${this.redisConfig.keyPrefix || 'coorchat:'}history`; + const messages = await this.publisher.lrange(historyKey, 0, limit - 1); + + const parsedMessages: Message[] = []; + for (const messageStr of messages) { + try { + const parsed = JSON.parse(messageStr); + const validationResult = validator.validate(parsed); + if (validationResult.valid) { + const message = parsed as Message; + + // Filter by timestamp if before is specified + if (!before || new Date(message.timestamp) < before) { + parsedMessages.push(message); + } + } + } catch { + // Skip invalid messages + } + } + + return parsedMessages; + } catch (error) { + this.logger.debug('Message history not available', { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } + } + + /** + * Store message in history (if history persistence is enabled) + */ + private async storeInHistory(message: Message): Promise { + try { + const historyKey = `${this.redisConfig.keyPrefix || 'coorchat:'}history`; + const messageStr = JSON.stringify(message); + + // Add to list (newest first) + await this.publisher.lpush(historyKey, messageStr); + + // Trim to keep only recent messages (e.g., 1000 messages) + await this.publisher.ltrim(historyKey, 0, 999); + } catch (error) { + this.logger.debug('Failed to store message in history', { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + /** + * Get Redis connection status + */ + getRedisStatus(): { + publisher: string; + subscriber: string; + } { + return { + publisher: this.publisher.status, + subscriber: this.subscriber.status, + }; + } +} + +// Register with factory +import { ChannelFactory } from '../base/ChannelFactory.js'; +ChannelFactory.register('redis', RedisChannel); diff --git a/packages/mcp-server/src/channels/signalr/SignalRChannel.ts b/packages/mcp-server/src/channels/signalr/SignalRChannel.ts new file mode 100644 index 0000000..d5b3086 --- /dev/null +++ b/packages/mcp-server/src/channels/signalr/SignalRChannel.ts @@ -0,0 +1,235 @@ +/** + * SignalRChannel - SignalR implementation of Channel interface + * Provides SignalR-based real-time communication for agent coordination + */ + +import * as signalR from '@microsoft/signalr'; +import { ChannelAdapter } from '../base/ChannelAdapter.js'; +import type { ChannelConfig } from '../base/Channel.js'; +import type { Message } from '../../protocol/Message.js'; +import { validator } from '../../protocol/MessageValidator.js'; + +/** + * SignalR-specific configuration + */ +export interface SignalRConfig { + hubUrl: string; + accessToken: string; +} + +/** + * SignalRChannel implementation + */ +export class SignalRChannel extends ChannelAdapter { + private connection: signalR.HubConnection; + private signalRConfig: SignalRConfig; + + constructor(config: ChannelConfig) { + super(config); + + this.signalRConfig = config.connectionParams as SignalRConfig; + + // Validate TLS/HTTPS usage for security + if (!this.signalRConfig.hubUrl.startsWith('https://')) { + this.logger.warn('SignalR hub URL is not using HTTPS - connection may be insecure', { + hubUrl: this.signalRConfig.hubUrl, + }); + } + + // Initialize SignalR connection with authentication and TLS + this.connection = new signalR.HubConnectionBuilder() + .withUrl(this.signalRConfig.hubUrl, { + accessTokenFactory: () => this.getAuthToken(), + // SignalR will use HTTPS automatically if URL starts with https:// + // Additional transport options can be configured here + skipNegotiation: false, + transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.ServerSentEvents, + }) + .withAutomaticReconnect({ + nextRetryDelayInMilliseconds: (retryContext) => { + // Exponential backoff + return Math.min(1000 * Math.pow(2, retryContext.previousRetryCount), 30000); + }, + }) + .configureLogging(signalR.LogLevel.Information) + .build(); + + this.setupEventHandlers(); + } + + /** + * Setup SignalR event handlers + */ + private setupEventHandlers(): void { + // Handle incoming messages + this.connection.on('ReceiveMessage', (messageJson: string) => { + this.handleSignalRMessage(messageJson).catch((error) => { + this.handleError( + error instanceof Error ? error : new Error(String(error)) + ); + }); + }); + + // Handle reconnecting + this.connection.onreconnecting((error) => { + this.logger.warn('SignalR reconnecting', { + error: error?.message, + }); + this.setStatus('reconnecting'); + }); + + // Handle reconnected + this.connection.onreconnected((connectionId) => { + this.logger.info('SignalR reconnected', { connectionId }); + this.setStatus('connected'); + }); + + // Handle close + this.connection.onclose((error) => { + this.logger.warn('SignalR connection closed', { + error: error?.message, + }); + this.setStatus('disconnected'); + }); + } + + /** + * Handle incoming SignalR message + */ + private async handleSignalRMessage(messageJson: string): Promise { + try { + // Parse message + const parsedMessage = JSON.parse(messageJson); + + // Validate message + const validationResult = validator.validateFull(parsedMessage); + if (!validationResult.valid) { + this.logger.warn('Invalid message received', { + errors: validator.getErrorSummary(validationResult), + }); + return; + } + + // Handle the message + this.handleMessage(parsedMessage as Message); + } catch (error) { + this.logger.error('Failed to parse SignalR message', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Connect to SignalR hub + */ + protected async doConnect(): Promise { + try { + await this.connection.start(); + this.logger.info('Connected to SignalR hub', { + hubUrl: this.signalRConfig.hubUrl, + connectionId: this.connection.connectionId, + }); + } catch (error) { + throw new Error( + `Failed to connect to SignalR hub: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + /** + * Disconnect from SignalR hub + */ + protected async doDisconnect(): Promise { + await this.connection.stop(); + } + + /** + * Send message via SignalR + */ + protected async doSendMessage(message: Message): Promise { + if (this.connection.state !== signalR.HubConnectionState.Connected) { + throw new Error(`Cannot send message: connection state is ${this.connection.state}`); + } + + // Serialize message to JSON + const messageJson = JSON.stringify(message); + + // Send via SignalR hub method + await this.connection.invoke('SendMessage', messageJson); + } + + /** + * Ping SignalR connection + */ + protected async doPing(): Promise { + if (this.connection.state !== signalR.HubConnectionState.Connected) { + throw new Error('SignalR connection not connected'); + } + + // Send a ping message to the hub + try { + await this.connection.invoke('Ping'); + } catch (error) { + throw new Error( + `SignalR ping failed: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + /** + * Get message history (if supported by hub) + */ + async getHistory(limit: number = 50, before?: Date): Promise { + try { + // Request history from hub + const messagesJson = await this.connection.invoke( + 'GetMessageHistory', + limit, + before?.toISOString() + ); + + const messages: Message[] = []; + for (const messageJson of messagesJson) { + try { + const parsed = JSON.parse(messageJson); + const validationResult = validator.validate(parsed); + if (validationResult.valid) { + messages.push(parsed as Message); + } + } catch { + // Skip invalid messages + } + } + + return messages; + } catch (error) { + this.logger.debug('Message history not supported by SignalR hub', { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } + } + + /** + * Get connection state + */ + getConnectionState(): signalR.HubConnectionState { + return this.connection.state; + } + + /** + * Get authentication token for SignalR + */ + protected getAuthToken(): string { + // Use the shared channel token for authentication + return this.config.token; + } +} + +// Register with factory +import { ChannelFactory } from '../base/ChannelFactory.js'; +ChannelFactory.register('signalr', SignalRChannel); diff --git a/packages/mcp-server/src/cli/index.ts b/packages/mcp-server/src/cli/index.ts new file mode 100644 index 0000000..e0f416c --- /dev/null +++ b/packages/mcp-server/src/cli/index.ts @@ -0,0 +1,381 @@ +#!/usr/bin/env node +/** + * CoorChat CLI - Command-line interface for managing agents + */ + +import { Command } from 'commander'; +import * as dotenv from 'dotenv'; +import { TokenGenerator } from '../config/TokenGenerator.js'; +import { ChannelFactory } from '../channels/base/ChannelFactory.js'; +import { AgentRegistry } from '../agents/AgentRegistry.js'; +import { RoleManager } from '../agents/RoleManager.js'; +import { TaskQueue } from '../tasks/TaskQueue.js'; +import type { ChannelConfig } from '../channels/base/Channel.js'; +import type { Agent } from '../agents/Agent.js'; + +// Load environment variables +dotenv.config(); + +const program = new Command(); + +program + .name('coorchat') + .description('CoorChat CLI - Multi-Agent Coordination System') + .version('1.0.0'); + +// Token commands +const tokenCmd = program.command('token').description('Token management commands'); + +tokenCmd + .command('generate') + .description('Generate a secure token') + .option('-t, --type ', 'Token type (channel, api, webhook)', 'channel') + .option('-c, --count ', 'Number of tokens to generate', '1') + .action((options) => { + const count = parseInt(options.count); + const tokens: string[] = []; + + for (let i = 0; i < count; i++) { + let token: string; + switch (options.type) { + case 'channel': + token = TokenGenerator.generateChannelToken(); + break; + case 'api': + token = TokenGenerator.generateAPIToken(); + break; + case 'webhook': + token = TokenGenerator.generateWebhookSecret(); + break; + default: + console.error(`Invalid token type: ${options.type}`); + process.exit(1); + } + tokens.push(token); + } + + console.log('Generated tokens:'); + tokens.forEach((token, index) => { + console.log(`${index + 1}. ${token}`); + }); + + if (count === 1) { + console.log('\nAdd to your .env file:'); + console.log(`SHARED_TOKEN=${tokens[0]}`); + } + }); + +tokenCmd + .command('validate ') + .description('Validate token format') + .action((token) => { + const isValid = TokenGenerator.validateFormat(token); + if (isValid) { + console.log('βœ… Token is valid'); + console.log(`Length: ${token.length} characters`); + if (token.startsWith('cct_')) { + console.log('Type: Channel Token'); + } else if (token.startsWith('cca_')) { + console.log('Type: API Token'); + } else { + console.log('Type: Generic'); + } + } else { + console.log('❌ Token is invalid'); + console.log('Requirements:'); + console.log(' - Minimum 16 characters'); + console.log(' - Alphanumeric with _ and -'); + console.log(' - No whitespace'); + } + }); + +tokenCmd + .command('hash ') + .description('Hash a token (SHA-256)') + .action((token) => { + const hash = TokenGenerator.hash(token); + console.log('Token hash:'); + console.log(hash); + }); + +// Agent commands +const agentCmd = program.command('agent').description('Agent management commands'); + +agentCmd + .command('start') + .description('Start an agent') + .option('-i, --id ', 'Agent ID') + .option('-r, --role ', 'Agent role', 'developer') + .option('-c, --channel ', 'Channel type', process.env.CHANNEL_TYPE || 'redis') + .action(async (options) => { + const agentId = options.id || `agent-${Date.now()}`; + const role = options.role; + + console.log(`πŸ€– Starting agent: ${agentId}`); + console.log(` Role: ${role}`); + console.log(` Channel: ${options.channel}`); + console.log(''); + + try { + // Create channel config + const config: ChannelConfig = { + type: options.channel, + token: process.env.SHARED_TOKEN || '', + connectionParams: getConnectionParams(options.channel), + }; + + // Validate token + if (!config.token || config.token.length < 16) { + console.error('❌ Invalid or missing SHARED_TOKEN in environment'); + console.error('Run: coorchat token generate'); + process.exit(1); + } + + // Create channel + const channel = ChannelFactory.create(config); + await channel.connect(); + + console.log('βœ… Connected to channel'); + + // Register agent + const agent: Agent = { + id: agentId, + role, + capabilities: [], + status: 'active', + metadata: { + platform: process.platform, + environment: 'CLI', + }, + }; + + // Listen for messages + channel.onMessage((message) => { + console.log(`πŸ“¨ [${message.messageType}] from ${message.senderId}`); + if (message.payload) { + console.log(` ${JSON.stringify(message.payload, null, 2)}`); + } + }); + + // Keep alive + console.log(''); + console.log('Agent is running. Press Ctrl+C to stop.'); + console.log(''); + + // Graceful shutdown + process.on('SIGINT', async () => { + console.log('\n\nπŸ›‘ Shutting down...'); + await channel.disconnect(); + console.log('βœ… Disconnected'); + process.exit(0); + }); + + // Keep process alive + await new Promise(() => {}); + } catch (error) { + console.error('❌ Failed to start agent:', error); + process.exit(1); + } + }); + +agentCmd + .command('list') + .description('List active agents') + .action(async () => { + // This would need a shared state store (Redis, etc.) + console.log('πŸ“‹ Active Agents:'); + console.log('(This feature requires a shared state store)'); + }); + +// Role commands +const roleCmd = program.command('role').description('Role management commands'); + +roleCmd + .command('list') + .description('List available roles') + .action(() => { + const roleManager = new RoleManager(); + const roles = roleManager.getPredefinedRoles(); + + console.log('πŸ“‹ Available Roles:\n'); + roles.forEach((role) => { + console.log(`${role.name}:`); + console.log(` Description: ${role.description}`); + console.log(` Capabilities: ${role.defaultCapabilities.join(', ')}`); + console.log(''); + }); + }); + +roleCmd + .command('suggest <...capabilities>') + .description('Suggest roles based on capabilities') + .action((capabilities) => { + const roleManager = new RoleManager(); + const suggestions = roleManager.suggestRole(capabilities); + + if (suggestions.length > 0) { + console.log('πŸ’‘ Suggested Roles:\n'); + suggestions.forEach((role, index) => { + console.log(`${index + 1}. ${role.name}`); + console.log(` ${role.description}`); + console.log(''); + }); + } else { + console.log('No matching roles found'); + } + }); + +// Config commands +const configCmd = program.command('config').description('Configuration commands'); + +configCmd + .command('show') + .description('Show current configuration') + .action(() => { + console.log('βš™οΈ Current Configuration:\n'); + console.log(`Channel Type: ${process.env.CHANNEL_TYPE || '(not set)'}`); + console.log(`Agent ID: ${process.env.AGENT_ID || '(not set)'}`); + console.log(`Agent Role: ${process.env.AGENT_ROLE || '(not set)'}`); + console.log(`Shared Token: ${process.env.SHARED_TOKEN ? '***' + process.env.SHARED_TOKEN.slice(-8) : '(not set)'}`); + console.log(''); + + if (process.env.CHANNEL_TYPE === 'redis') { + console.log('Redis Configuration:'); + console.log(` Host: ${process.env.REDIS_HOST || 'localhost'}`); + console.log(` Port: ${process.env.REDIS_PORT || '6379'}`); + console.log(` TLS: ${process.env.REDIS_TLS || 'false'}`); + } else if (process.env.CHANNEL_TYPE === 'discord') { + console.log('Discord Configuration:'); + console.log(` Bot Token: ${process.env.DISCORD_BOT_TOKEN ? '***' : '(not set)'}`); + console.log(` Channel ID: ${process.env.DISCORD_CHANNEL_ID || '(not set)'}`); + } else if (process.env.CHANNEL_TYPE === 'signalr') { + console.log('SignalR Configuration:'); + console.log(` Hub URL: ${process.env.SIGNALR_HUB_URL || '(not set)'}`); + } + + console.log(''); + + if (process.env.GITHUB_TOKEN) { + console.log('GitHub Integration:'); + console.log(` Token: ***${process.env.GITHUB_TOKEN.slice(-8)}`); + console.log(` Owner: ${process.env.GITHUB_OWNER || '(not set)'}`); + console.log(` Repo: ${process.env.GITHUB_REPO || '(not set)'}`); + console.log(''); + } + }); + +configCmd + .command('init') + .description('Initialize configuration file') + .option('-c, --channel ', 'Channel type (redis, discord, signalr)', 'redis') + .action((options) => { + const token = TokenGenerator.generateChannelToken(); + + const envContent = `# CoorChat Configuration +# Generated: ${new Date().toISOString()} + +# Shared authentication token (use same token for all agents) +SHARED_TOKEN=${token} + +# Channel configuration +CHANNEL_TYPE=${options.channel} +${options.channel === 'redis' ? `REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_TLS=false` : ''} +${options.channel === 'discord' ? `DISCORD_BOT_TOKEN=your_bot_token_here +DISCORD_CHANNEL_ID=your_channel_id_here` : ''} +${options.channel === 'signalr' ? `SIGNALR_HUB_URL=https://localhost:5001/agentHub` : ''} + +# Agent configuration +AGENT_ID=agent-${Date.now()} +AGENT_ROLE=developer + +# Optional: GitHub integration +# GITHUB_TOKEN=ghp_your_token_here +# GITHUB_OWNER=your-org +# GITHUB_REPO=your-repo + +# Logging +LOG_LEVEL=info +`; + + console.log(envContent); + console.log('πŸ’Ύ Save this to .env file in packages/mcp-server/'); + }); + +// Monitor command +program + .command('monitor') + .description('Monitor agent coordination activity') + .option('-c, --channel ', 'Channel type', process.env.CHANNEL_TYPE || 'redis') + .action(async (options) => { + console.log('πŸ‘οΈ CoorChat Monitor\n'); + console.log('Listening for agent activity...\n'); + + try { + const config: ChannelConfig = { + type: options.channel, + token: process.env.SHARED_TOKEN || '', + connectionParams: getConnectionParams(options.channel), + }; + + const channel = ChannelFactory.create(config); + await channel.connect(); + + console.log(`βœ… Connected to ${options.channel} channel\n`); + + channel.onMessage((message) => { + const timestamp = new Date(message.timestamp).toLocaleTimeString(); + console.log(`[${timestamp}] ${message.messageType}`); + console.log(` From: ${message.senderId}`); + if (message.recipientId) { + console.log(` To: ${message.recipientId}`); + } + if (message.payload) { + console.log(` Payload: ${JSON.stringify(message.payload, null, 2)}`); + } + console.log(''); + }); + + // Graceful shutdown + process.on('SIGINT', async () => { + console.log('\n\nπŸ›‘ Stopping monitor...'); + await channel.disconnect(); + console.log('βœ… Disconnected'); + process.exit(0); + }); + + // Keep alive + await new Promise(() => {}); + } catch (error) { + console.error('❌ Failed to start monitor:', error); + process.exit(1); + } + }); + +// Helper function to get connection params +function getConnectionParams(channelType: string): any { + switch (channelType) { + case 'redis': + return { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD, + tls: process.env.REDIS_TLS === 'true', + }; + case 'discord': + return { + botToken: process.env.DISCORD_BOT_TOKEN, + channelId: process.env.DISCORD_CHANNEL_ID, + }; + case 'signalr': + return { + hubUrl: process.env.SIGNALR_HUB_URL || 'https://localhost:5001/agentHub', + }; + default: + return {}; + } +} + +// Parse CLI arguments +program.parse(); diff --git a/packages/mcp-server/src/config/ConfigLoader.ts b/packages/mcp-server/src/config/ConfigLoader.ts new file mode 100644 index 0000000..646effd --- /dev/null +++ b/packages/mcp-server/src/config/ConfigLoader.ts @@ -0,0 +1,242 @@ +/** + * ConfigLoader - Load configuration from JSON/YAML files with environment variable substitution + */ + +import { readFileSync, existsSync } from 'fs'; +import { parse as parseYAML } from 'yaml'; +import { EnvironmentResolver } from './EnvironmentResolver.js'; + +/** + * Configuration file format + */ +export type ConfigFormat = 'json' | 'yaml' | 'yml'; + +/** + * Load options + */ +export interface LoadOptions { + /** Whether to resolve environment variables */ + resolveEnv?: boolean; + + /** Whether to throw on missing file */ + throwOnMissing?: boolean; + + /** Default config to merge with loaded config */ + defaults?: Record; + + /** Encoding for reading files */ + encoding?: BufferEncoding; +} + +/** + * ConfigLoader class for loading configuration files + */ +export class ConfigLoader { + private resolver: EnvironmentResolver; + + constructor() { + this.resolver = new EnvironmentResolver(); + } + + /** + * Load configuration from a file + * @param filePath - Path to configuration file + * @param options - Load options + * @returns Parsed configuration object + */ + load>( + filePath: string, + options: LoadOptions = {} + ): T { + const { + resolveEnv = true, + throwOnMissing = true, + defaults = {}, + encoding = 'utf-8', + } = options; + + // Check if file exists + if (!existsSync(filePath)) { + if (throwOnMissing) { + throw new Error(`Configuration file not found: ${filePath}`); + } + return defaults as T; + } + + // Read file content + let content: string; + try { + content = readFileSync(filePath, encoding); + } catch (error) { + throw new Error( + `Failed to read configuration file: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + + // Resolve environment variables if requested + if (resolveEnv) { + content = this.resolver.resolve(content); + } + + // Determine format from file extension + const format = this.detectFormat(filePath); + + // Parse content based on format + let config: Record; + try { + config = this.parse(content, format); + } catch (error) { + throw new Error( + `Failed to parse configuration file (${format}): ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + + // Merge with defaults + return this.mergeDeep(defaults, config) as T; + } + + /** + * Load configuration from multiple files (cascade) + * Later files override earlier files + */ + loadMultiple>( + filePaths: string[], + options: LoadOptions = {} + ): T { + let merged: Record = options.defaults || {}; + + for (const filePath of filePaths) { + try { + const config = this.load(filePath, { + ...options, + throwOnMissing: false, + defaults: {}, + }); + merged = this.mergeDeep(merged, config); + } catch (error) { + if (options.throwOnMissing) { + throw error; + } + // Skip files that can't be loaded if throwOnMissing is false + } + } + + return merged as T; + } + + /** + * Detect configuration format from file extension + */ + private detectFormat(filePath: string): ConfigFormat { + const ext = filePath.split('.').pop()?.toLowerCase(); + + switch (ext) { + case 'json': + return 'json'; + case 'yaml': + case 'yml': + return 'yaml'; + default: + throw new Error( + `Unknown configuration file format: ${ext}. Supported formats: json, yaml, yml` + ); + } + } + + /** + * Parse configuration content based on format + */ + private parse(content: string, format: ConfigFormat): Record { + switch (format) { + case 'json': + return JSON.parse(content); + + case 'yaml': + case 'yml': + return parseYAML(content) as Record; + + default: + throw new Error(`Unsupported format: ${format}`); + } + } + + /** + * Deep merge two objects + * Later object properties override earlier object properties + */ + private mergeDeep( + target: Record, + source: Record + ): Record { + const result = { ...target }; + + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + const sourceValue = source[key]; + const targetValue = result[key]; + + if ( + this.isPlainObject(sourceValue) && + this.isPlainObject(targetValue) + ) { + result[key] = this.mergeDeep( + targetValue as Record, + sourceValue as Record + ); + } else { + result[key] = sourceValue; + } + } + } + + return result; + } + + /** + * Check if a value is a plain object + */ + private isPlainObject(value: unknown): boolean { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); + } + + /** + * Get environment resolver instance + */ + getResolver(): EnvironmentResolver { + return this.resolver; + } +} + +/** + * Singleton config loader instance + */ +export const configLoader = new ConfigLoader(); + +/** + * Convenience function to load configuration + */ +export function loadConfig>( + filePath: string, + options?: LoadOptions +): T { + return configLoader.load(filePath, options); +} + +/** + * Convenience function to load multiple configuration files + */ +export function loadMultipleConfigs>( + filePaths: string[], + options?: LoadOptions +): T { + return configLoader.loadMultiple(filePaths, options); +} diff --git a/packages/mcp-server/src/config/ConfigValidator.ts b/packages/mcp-server/src/config/ConfigValidator.ts new file mode 100644 index 0000000..9fb2bf9 --- /dev/null +++ b/packages/mcp-server/src/config/ConfigValidator.ts @@ -0,0 +1,269 @@ +/** + * ConfigValidator - Validate configuration using Zod schemas + * Based on specs/001-multi-agent-coordination/plan.md configuration requirements + */ + +import { z } from 'zod'; + +/** + * Retry configuration schema + */ +export const RetryConfigSchema = z.object({ + enabled: z.boolean().default(true), + maxAttempts: z.number().int().positive().default(5), + initialDelayMs: z.number().int().positive().default(1000), + maxDelayMs: z.number().int().positive().default(60000), +}); + +/** + * Heartbeat configuration schema + */ +export const HeartbeatConfigSchema = z.object({ + enabled: z.boolean().default(true), + intervalMs: z.number().int().positive().default(15000), + timeoutMs: z.number().int().positive().default(30000), +}); + +/** + * Discord channel configuration schema + */ +export const DiscordConfigSchema = z.object({ + guildId: z.string().min(1), + channelId: z.string().min(1), + botToken: z.string().min(1), +}); + +/** + * SignalR channel configuration schema + */ +export const SignalRConfigSchema = z.object({ + hubUrl: z.string().url(), + accessToken: z.string().min(1), +}); + +/** + * Redis channel configuration schema + */ +export const RedisConfigSchema = z.object({ + host: z.string().min(1).default('localhost'), + port: z.number().int().positive().default(6379), + password: z.string().optional(), + db: z.number().int().min(0).default(0), + keyPrefix: z.string().default('coorchat:'), + tls: z.boolean().default(false), +}); + +/** + * Relay channel configuration schema + */ +export const RelayConfigSchema = z.object({ + serverUrl: z.string().url(), + accessToken: z.string().min(1), + channelId: z.string().uuid(), +}); + +/** + * Channel configuration schema (discriminated union) + */ +export const ChannelConfigSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('discord'), + token: z.string().min(1), + connectionParams: DiscordConfigSchema, + retry: RetryConfigSchema.optional(), + heartbeat: HeartbeatConfigSchema.optional(), + }), + z.object({ + type: z.literal('signalr'), + token: z.string().min(1), + connectionParams: SignalRConfigSchema, + retry: RetryConfigSchema.optional(), + heartbeat: HeartbeatConfigSchema.optional(), + }), + z.object({ + type: z.literal('redis'), + token: z.string().min(1), + connectionParams: RedisConfigSchema, + retry: RetryConfigSchema.optional(), + heartbeat: HeartbeatConfigSchema.optional(), + }), + z.object({ + type: z.literal('relay'), + token: z.string().min(1), + connectionParams: RelayConfigSchema, + retry: RetryConfigSchema.optional(), + heartbeat: HeartbeatConfigSchema.optional(), + }), +]); + +/** + * GitHub configuration schema + */ +export const GitHubConfigSchema = z.object({ + token: z.string().min(1), + owner: z.string().min(1), + repo: z.string().min(1), + webhookSecret: z.string().optional(), + webhookPort: z.number().int().positive().default(3000), + pollingEnabled: z.boolean().default(true), + pollingIntervalMs: z.number().int().positive().default(30000), +}); + +/** + * Agent configuration schema + */ +export const AgentConfigSchema = z.object({ + role: z.string().min(1).max(50), + platform: z.enum(['Linux', 'macOS', 'Windows']), + environment: z.string().min(1).max(100), + tools: z.array(z.string()).min(1), + languages: z.array(z.string()).optional(), + apiAccess: z.array(z.string()).optional(), + resourceLimits: z + .object({ + apiQuotaPerHour: z.number().int().nonnegative().optional(), + maxConcurrentTasks: z.number().int().min(1).max(10).default(1), + rateLimitPerMinute: z.number().int().nonnegative().optional(), + memoryLimitMB: z.number().int().nonnegative().optional(), + }) + .optional(), +}); + +/** + * Logging configuration schema + */ +export const LoggingConfigSchema = z.object({ + level: z.enum(['ERROR', 'WARN', 'INFO', 'DEBUG']).default('INFO'), + format: z.enum(['json', 'text']).default('json'), + output: z.enum(['console', 'file', 'both']).default('console'), + filePath: z.string().optional(), +}); + +/** + * Main application configuration schema + */ +export const AppConfigSchema = z.object({ + channel: ChannelConfigSchema, + github: GitHubConfigSchema.optional(), + agent: AgentConfigSchema, + logging: LoggingConfigSchema.optional(), +}); + +/** + * Type exports + */ +export type RetryConfig = z.infer; +export type HeartbeatConfig = z.infer; +export type DiscordConfig = z.infer; +export type SignalRConfig = z.infer; +export type RedisConfig = z.infer; +export type RelayConfig = z.infer; +export type ChannelConfig = z.infer; +export type GitHubConfig = z.infer; +export type AgentConfig = z.infer; +export type LoggingConfig = z.infer; +export type AppConfig = z.infer; + +/** + * Validation result + */ +export interface ValidationResult { + success: boolean; + data?: T; + errors?: z.ZodError; +} + +/** + * ConfigValidator class + */ +export class ConfigValidator { + /** + * Validate configuration against a schema + */ + validate(schema: z.ZodSchema, data: unknown): ValidationResult { + const result = schema.safeParse(data); + + if (result.success) { + return { + success: true, + data: result.data, + }; + } + + return { + success: false, + errors: result.error, + }; + } + + /** + * Validate and throw on error + */ + validateOrThrow(schema: z.ZodSchema, data: unknown): T { + return schema.parse(data); + } + + /** + * Validate application configuration + */ + validateAppConfig(data: unknown): ValidationResult { + return this.validate(AppConfigSchema, data); + } + + /** + * Validate channel configuration + */ + validateChannelConfig(data: unknown): ValidationResult { + return this.validate(ChannelConfigSchema, data); + } + + /** + * Validate GitHub configuration + */ + validateGitHubConfig(data: unknown): ValidationResult { + return this.validate(GitHubConfigSchema, data); + } + + /** + * Validate agent configuration + */ + validateAgentConfig(data: unknown): ValidationResult { + return this.validate(AgentConfigSchema, data); + } + + /** + * Get formatted error messages + */ + formatErrors(errors: z.ZodError): string[] { + return errors.errors.map((err) => { + const path = err.path.join('.'); + return `${path}: ${err.message}`; + }); + } + + /** + * Get error summary + */ + getErrorSummary(errors: z.ZodError): string { + return this.formatErrors(errors).join('; '); + } +} + +/** + * Singleton validator instance + */ +export const validator = new ConfigValidator(); + +/** + * Convenience function to validate app configuration + */ +export function validateAppConfig(data: unknown): AppConfig { + return validator.validateOrThrow(AppConfigSchema, data); +} + +/** + * Convenience function to validate channel configuration + */ +export function validateChannelConfig(data: unknown): ChannelConfig { + return validator.validateOrThrow(ChannelConfigSchema, data); +} diff --git a/packages/mcp-server/src/config/EnvironmentResolver.ts b/packages/mcp-server/src/config/EnvironmentResolver.ts new file mode 100644 index 0000000..4809593 --- /dev/null +++ b/packages/mcp-server/src/config/EnvironmentResolver.ts @@ -0,0 +1,211 @@ +/** + * EnvironmentResolver - Resolve environment variable placeholders in configuration + * Supports ${VAR_NAME}, ${VAR_NAME:-default}, and ${VAR_NAME:?error message} syntax + */ + +/** + * Environment variable placeholder patterns + */ +const ENV_VAR_PATTERN = /\$\{([^}:]+)(?:([:-])([^}]+))?\}/g; + +/** + * Resolution options + */ +export interface ResolverOptions { + /** Whether to throw on undefined variables */ + throwOnUndefined?: boolean; + + /** Custom environment variables (overrides process.env) */ + env?: Record; + + /** Whether to resolve recursively */ + recursive?: boolean; + + /** Maximum recursion depth (prevents infinite loops) */ + maxDepth?: number; +} + +/** + * EnvironmentResolver class for resolving environment variable placeholders + */ +export class EnvironmentResolver { + private options: Required; + + constructor(options: ResolverOptions = {}) { + this.options = { + throwOnUndefined: false, + env: process.env, + recursive: true, + maxDepth: 10, + ...options, + }; + } + + /** + * Resolve environment variables in a string + * Supported syntax: + * - ${VAR_NAME} - Simple substitution + * - ${VAR_NAME:-default} - Use default if undefined + * - ${VAR_NAME:?error message} - Throw error if undefined + * + * @param input - String containing environment variable placeholders + * @param depth - Current recursion depth (internal) + * @returns Resolved string + */ + resolve(input: string, depth: number = 0): string { + if (depth >= this.options.maxDepth) { + throw new Error( + `Maximum recursion depth (${this.options.maxDepth}) exceeded while resolving environment variables` + ); + } + + let hasUnresolved = false; + + const resolved = input.replace( + ENV_VAR_PATTERN, + (match, varName, operator, operand) => { + const value = this.options.env[varName.trim()]; + + // Handle :- operator (default value) + if (operator === '-') { + return value !== undefined ? value : operand || ''; + } + + // Handle :? operator (required with error message) + if (operator === '?') { + if (value === undefined) { + throw new Error(operand || `Required environment variable not set: ${varName}`); + } + return value; + } + + // Simple ${VAR_NAME} substitution + if (value === undefined) { + if (this.options.throwOnUndefined) { + throw new Error(`Undefined environment variable: ${varName}`); + } + hasUnresolved = true; + return match; // Keep placeholder if undefined + } + + return value; + } + ); + + // Recursively resolve if needed and if there were changes + if (this.options.recursive && resolved !== input && hasUnresolved) { + return this.resolve(resolved, depth + 1); + } + + return resolved; + } + + /** + * Resolve environment variables in an object (deep) + * @param obj - Object containing values with environment variable placeholders + * @returns Object with resolved values + */ + resolveObject(obj: T): T { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => this.resolveObject(item)) as T; + } + + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'string') { + result[key] = this.resolve(value); + } else if (typeof value === 'object' && value !== null) { + result[key] = this.resolveObject(value); + } else { + result[key] = value; + } + } + + return result as T; + } + + /** + * Check if a string contains environment variable placeholders + */ + hasPlaceholders(input: string): boolean { + return ENV_VAR_PATTERN.test(input); + } + + /** + * Extract all environment variable names from a string + */ + extractVariables(input: string): string[] { + const variables: string[] = []; + const regex = new RegExp(ENV_VAR_PATTERN.source, 'g'); + let match; + + while ((match = regex.exec(input)) !== null) { + variables.push(match[1].trim()); + } + + return variables; + } + + /** + * Validate that all required environment variables are set + * @param input - String or object to check + * @returns Array of missing variable names + */ + findMissingVariables(input: string | Record): string[] { + const inputStr = typeof input === 'string' ? input : JSON.stringify(input); + const variables = this.extractVariables(inputStr); + const missing: string[] = []; + + for (const varName of variables) { + if (this.options.env[varName] === undefined) { + missing.push(varName); + } + } + + return [...new Set(missing)]; // Remove duplicates + } + + /** + * Get current environment (or custom env if provided) + */ + getEnv(): Record { + return this.options.env; + } + + /** + * Update resolver options + */ + setOptions(options: Partial): void { + this.options = { ...this.options, ...options }; + } +} + +/** + * Singleton resolver instance + */ +export const resolver = new EnvironmentResolver(); + +/** + * Convenience function to resolve environment variables in a string + */ +export function resolveEnv(input: string, options?: ResolverOptions): string { + if (options) { + return new EnvironmentResolver(options).resolve(input); + } + return resolver.resolve(input); +} + +/** + * Convenience function to resolve environment variables in an object + */ +export function resolveEnvObject(obj: T, options?: ResolverOptions): T { + if (options) { + return new EnvironmentResolver(options).resolveObject(obj); + } + return resolver.resolveObject(obj); +} diff --git a/packages/mcp-server/src/config/TokenGenerator.ts b/packages/mcp-server/src/config/TokenGenerator.ts new file mode 100644 index 0000000..5053710 --- /dev/null +++ b/packages/mcp-server/src/config/TokenGenerator.ts @@ -0,0 +1,171 @@ +/** + * TokenGenerator - Generate secure random tokens for authentication + * Uses crypto.randomBytes for cryptographically secure token generation + */ + +import { randomBytes, createHash } from 'crypto'; + +/** + * Token generation options + */ +export interface TokenOptions { + /** Token length in bytes (default: 32) */ + length?: number; + + /** Output encoding (default: 'hex') */ + encoding?: 'hex' | 'base64' | 'base64url'; + + /** Whether to include a prefix */ + prefix?: string; +} + +/** + * TokenGenerator class + */ +export class TokenGenerator { + /** + * Generate a secure random token + */ + static generate(options: TokenOptions = {}): string { + const { + length = 32, + encoding = 'hex', + prefix = '', + } = options; + + // Generate random bytes + const bytes = randomBytes(length); + + // Convert to string based on encoding + let token: string; + switch (encoding) { + case 'hex': + token = bytes.toString('hex'); + break; + case 'base64': + token = bytes.toString('base64'); + break; + case 'base64url': + token = bytes.toString('base64url'); + break; + default: + token = bytes.toString('hex'); + } + + // Add prefix if specified + return prefix ? `${prefix}${token}` : token; + } + + /** + * Generate a channel token (128-bit security) + */ + static generateChannelToken(): string { + return this.generate({ + length: 32, + encoding: 'hex', + prefix: 'cct_', + }); + } + + /** + * Generate an API token (256-bit security) + */ + static generateAPIToken(): string { + return this.generate({ + length: 64, + encoding: 'base64url', + prefix: 'cca_', + }); + } + + /** + * Generate a webhook secret (256-bit security) + */ + static generateWebhookSecret(): string { + return this.generate({ + length: 64, + encoding: 'hex', + }); + } + + /** + * Hash a token (for secure storage) + */ + static hash(token: string): string { + return createHash('sha256').update(token).digest('hex'); + } + + /** + * Validate token format + */ + static validateFormat(token: string, options: { + minLength?: number; + prefix?: string; + } = {}): boolean { + const { minLength = 16, prefix } = options; + + // Check for null/undefined + if (!token) { + return false; + } + + // Check minimum length + if (token.length < minLength) { + return false; + } + + // Check prefix if specified + if (prefix && !token.startsWith(prefix)) { + return false; + } + + // Check for valid characters (alphanumeric + URL-safe characters) + if (!/^[a-zA-Z0-9_-]+$/.test(token)) { + return false; + } + + return true; + } + + /** + * Generate a nonce (number used once) + */ + static generateNonce(): string { + return this.generate({ + length: 16, + encoding: 'hex', + }); + } + + /** + * Generate multiple tokens at once + */ + static generateBatch(count: number, options: TokenOptions = {}): string[] { + const tokens: string[] = []; + for (let i = 0; i < count; i++) { + tokens.push(this.generate(options)); + } + return tokens; + } +} + +/** + * Convenience function to generate a token + */ +export function generateToken(options?: TokenOptions): string { + return TokenGenerator.generate(options); +} + +/** + * Convenience function to generate a channel token + */ +export function generateChannelToken(): string { + return TokenGenerator.generateChannelToken(); +} + +/** + * Convenience function to hash a token + */ +export function hashToken(token: string): string { + return TokenGenerator.hash(token); +} diff --git a/packages/mcp-server/src/github/GitHubClient.ts b/packages/mcp-server/src/github/GitHubClient.ts new file mode 100644 index 0000000..4d85e61 --- /dev/null +++ b/packages/mcp-server/src/github/GitHubClient.ts @@ -0,0 +1,367 @@ +/** + * GitHubClient - Wrapper for GitHub API using @octokit/rest + * Provides simplified interface for fetching issues, PRs, and managing webhooks + */ + +import { Octokit } from '@octokit/rest'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * GitHub issue data + */ +export interface GitHubIssue { + id: number; + number: number; + title: string; + body: string | null; + state: 'open' | 'closed'; + url: string; + htmlUrl: string; + createdAt: Date; + updatedAt: Date; + labels: string[]; + assignees: string[]; +} + +/** + * GitHub pull request data + */ +export interface GitHubPullRequest { + id: number; + number: number; + title: string; + body: string | null; + state: 'open' | 'closed'; + url: string; + htmlUrl: string; + createdAt: Date; + updatedAt: Date; + head: string; + base: string; + mergeable: boolean | null; +} + +/** + * GitHub webhook event + */ +export interface GitHubWebhookEvent { + type: 'issues' | 'pull_request' | 'push' | 'unknown'; + action?: string; + issue?: GitHubIssue; + pullRequest?: GitHubPullRequest; + repository: { + owner: string; + name: string; + }; +} + +/** + * GitHub client configuration + */ +export interface GitHubClientConfig { + token: string; + owner: string; + repo: string; + logger?: Logger; +} + +/** + * GitHubClient class + */ +export class GitHubClient { + private octokit: Octokit; + private owner: string; + private repo: string; + private logger: Logger; + + constructor(config: GitHubClientConfig) { + this.octokit = new Octokit({ + auth: config.token, + }); + this.owner = config.owner; + this.repo = config.repo; + this.logger = config.logger || createLogger(); + } + + /** + * Get a single issue by number + */ + async getIssue(issueNumber: number): Promise { + try { + const { data } = await this.octokit.rest.issues.get({ + owner: this.owner, + repo: this.repo, + issue_number: issueNumber, + }); + + return this.mapIssue(data); + } catch (error) { + this.logger.error(`Failed to fetch issue #${issueNumber}`, { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * List issues with optional filters + */ + async listIssues(options?: { + state?: 'open' | 'closed' | 'all'; + labels?: string[]; + since?: Date; + perPage?: number; + }): Promise { + try { + const { data } = await this.octokit.rest.issues.listForRepo({ + owner: this.owner, + repo: this.repo, + state: options?.state || 'open', + labels: options?.labels?.join(','), + since: options?.since?.toISOString(), + per_page: options?.perPage || 30, + }); + + return data.filter((issue) => !issue.pull_request).map(this.mapIssue); + } catch (error) { + this.logger.error('Failed to list issues', { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Get a single pull request by number + */ + async getPullRequest(prNumber: number): Promise { + try { + const { data } = await this.octokit.rest.pulls.get({ + owner: this.owner, + repo: this.repo, + pull_number: prNumber, + }); + + return this.mapPullRequest(data); + } catch (error) { + this.logger.error(`Failed to fetch PR #${prNumber}`, { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * List pull requests with optional filters + */ + async listPullRequests(options?: { + state?: 'open' | 'closed' | 'all'; + head?: string; + base?: string; + perPage?: number; + }): Promise { + try { + const { data } = await this.octokit.rest.pulls.list({ + owner: this.owner, + repo: this.repo, + state: options?.state || 'open', + head: options?.head, + base: options?.base, + per_page: options?.perPage || 30, + }); + + return data.map(this.mapPullRequest); + } catch (error) { + this.logger.error('Failed to list pull requests', { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Create a comment on an issue + */ + async createIssueComment(issueNumber: number, body: string): Promise { + try { + await this.octokit.rest.issues.createComment({ + owner: this.owner, + repo: this.repo, + issue_number: issueNumber, + body, + }); + + this.logger.info(`Created comment on issue #${issueNumber}`); + } catch (error) { + this.logger.error(`Failed to create comment on issue #${issueNumber}`, { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Update issue labels + */ + async updateIssueLabels( + issueNumber: number, + labels: string[] + ): Promise { + try { + await this.octokit.rest.issues.setLabels({ + owner: this.owner, + repo: this.repo, + issue_number: issueNumber, + labels, + }); + + this.logger.info(`Updated labels on issue #${issueNumber}`); + } catch (error) { + this.logger.error(`Failed to update labels on issue #${issueNumber}`, { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Assign issue to users + */ + async assignIssue(issueNumber: number, assignees: string[]): Promise { + try { + await this.octokit.rest.issues.addAssignees({ + owner: this.owner, + repo: this.repo, + issue_number: issueNumber, + assignees, + }); + + this.logger.info(`Assigned issue #${issueNumber} to ${assignees.join(', ')}`); + } catch (error) { + this.logger.error(`Failed to assign issue #${issueNumber}`, { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Close an issue + */ + async closeIssue(issueNumber: number): Promise { + try { + await this.octokit.rest.issues.update({ + owner: this.owner, + repo: this.repo, + issue_number: issueNumber, + state: 'closed', + }); + + this.logger.info(`Closed issue #${issueNumber}`); + } catch (error) { + this.logger.error(`Failed to close issue #${issueNumber}`, { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Get repository information + */ + async getRepository(): Promise<{ owner: string; name: string; url: string }> { + try { + const { data } = await this.octokit.rest.repos.get({ + owner: this.owner, + repo: this.repo, + }); + + return { + owner: data.owner.login, + name: data.name, + url: data.html_url, + }; + } catch (error) { + this.logger.error('Failed to fetch repository info', { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Check rate limit status + */ + async getRateLimit(): Promise<{ + limit: number; + remaining: number; + reset: Date; + }> { + try { + const { data } = await this.octokit.rest.rateLimit.get(); + + return { + limit: data.rate.limit, + remaining: data.rate.remaining, + reset: new Date(data.rate.reset * 1000), + }; + } catch (error) { + this.logger.error('Failed to fetch rate limit', { + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + + /** + * Map Octokit issue to our GitHubIssue interface + */ + private mapIssue(data: any): GitHubIssue { + return { + id: data.id, + number: data.number, + title: data.title, + body: data.body, + state: data.state as 'open' | 'closed', + url: data.url, + htmlUrl: data.html_url, + createdAt: new Date(data.created_at), + updatedAt: new Date(data.updated_at), + labels: data.labels.map((label: any) => + typeof label === 'string' ? label : label.name + ), + assignees: data.assignees.map((assignee: any) => assignee.login), + }; + } + + /** + * Map Octokit PR to our GitHubPullRequest interface + */ + private mapPullRequest(data: any): GitHubPullRequest { + return { + id: data.id, + number: data.number, + title: data.title, + body: data.body, + state: data.state as 'open' | 'closed', + url: data.url, + htmlUrl: data.html_url, + createdAt: new Date(data.created_at), + updatedAt: new Date(data.updated_at), + head: data.head.ref, + base: data.base.ref, + mergeable: data.mergeable, + }; + } + + /** + * Get owner and repo + */ + getRepoInfo(): { owner: string; repo: string } { + return { + owner: this.owner, + repo: this.repo, + }; + } +} diff --git a/packages/mcp-server/src/github/PollingService.ts b/packages/mcp-server/src/github/PollingService.ts new file mode 100644 index 0000000..927bf6a --- /dev/null +++ b/packages/mcp-server/src/github/PollingService.ts @@ -0,0 +1,310 @@ +/** + * PollingService - Fallback polling with conditional requests (ETags) + * Polls GitHub API at regular intervals with optimization using ETags + */ + +import type { GitHubClient, GitHubIssue, GitHubPullRequest } from './GitHubClient.js'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Polling event types + */ +export type PollingEventType = 'issue_updated' | 'pr_updated' | 'new_issue' | 'new_pr'; + +/** + * Polling event + */ +export interface PollingEvent { + type: PollingEventType; + issue?: GitHubIssue; + pullRequest?: GitHubPullRequest; + previousState?: any; +} + +/** + * Polling event handler + */ +export type PollingEventHandler = (event: PollingEvent) => void | Promise; + +/** + * Polling service configuration + */ +export interface PollingServiceConfig { + /** GitHub client instance */ + client: GitHubClient; + + /** Polling interval in milliseconds */ + intervalMs?: number; + + /** Logger instance */ + logger?: Logger; + + /** Whether to poll issues */ + pollIssues?: boolean; + + /** Whether to poll pull requests */ + pollPullRequests?: boolean; +} + +/** + * Cached state for conditional requests + */ +interface CachedState { + etag?: string; + lastModified?: Date; + data: Map; +} + +/** + * PollingService class + */ +export class PollingService { + private client: GitHubClient; + private config: Required>; + private logger: Logger; + private eventHandlers: Set; + private timer?: NodeJS.Timeout; + private isRunning: boolean; + private issueCache: CachedState; + private prCache: CachedState; + + constructor(config: PollingServiceConfig) { + this.client = config.client; + this.config = { + intervalMs: config.intervalMs || 30000, // 30 seconds default + logger: config.logger || createLogger(), + pollIssues: config.pollIssues ?? true, + pollPullRequests: config.pollPullRequests ?? true, + }; + this.logger = this.config.logger; + this.eventHandlers = new Set(); + this.isRunning = false; + this.issueCache = { data: new Map() }; + this.prCache = { data: new Map() }; + } + + /** + * Start polling + */ + start(): void { + if (this.isRunning) { + this.logger.warn('Polling service already running'); + return; + } + + this.isRunning = true; + this.logger.info('Starting polling service', { + intervalMs: this.config.intervalMs, + }); + + // Run initial poll immediately + this.poll().catch((error) => { + this.logger.error('Error in initial poll', { + error: error instanceof Error ? error : new Error(String(error)), + }); + }); + + // Schedule periodic polling + this.timer = setInterval(() => { + this.poll().catch((error) => { + this.logger.error('Error in polling', { + error: error instanceof Error ? error : new Error(String(error)), + }); + }); + }, this.config.intervalMs); + } + + /** + * Stop polling + */ + stop(): void { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + + this.logger.info('Stopped polling service'); + } + + /** + * Perform a poll + */ + private async poll(): Promise { + const startTime = Date.now(); + + try { + // Poll issues if enabled + if (this.config.pollIssues) { + await this.pollIssues(); + } + + // Poll pull requests if enabled + if (this.config.pollPullRequests) { + await this.pollPullRequests(); + } + + const duration = Date.now() - startTime; + this.logger.debug('Poll completed', { duration }); + } catch (error) { + this.logger.error('Poll failed', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Poll issues + */ + private async pollIssues(): Promise { + try { + // Fetch issues (GitHub API will use If-None-Match header with ETag automatically) + const since = this.issueCache.lastModified; + const issues = await this.client.listIssues({ + state: 'open', + since, + }); + + // Detect changes + const newData = new Map(); + for (const issue of issues) { + newData.set(issue.number, issue); + + const cached = this.issueCache.data.get(issue.number); + if (!cached) { + // New issue + await this.notifyHandlers({ + type: 'new_issue', + issue, + }); + } else if (cached.updatedAt.getTime() !== issue.updatedAt.getTime()) { + // Updated issue + await this.notifyHandlers({ + type: 'issue_updated', + issue, + previousState: cached, + }); + } + } + + // Update cache + this.issueCache.data = newData; + this.issueCache.lastModified = new Date(); + } catch (error) { + this.logger.error('Error polling issues', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Poll pull requests + */ + private async pollPullRequests(): Promise { + try { + const pullRequests = await this.client.listPullRequests({ + state: 'open', + }); + + // Detect changes + const newData = new Map(); + for (const pr of pullRequests) { + newData.set(pr.number, pr); + + const cached = this.prCache.data.get(pr.number); + if (!cached) { + // New PR + await this.notifyHandlers({ + type: 'new_pr', + pullRequest: pr, + }); + } else if (cached.updatedAt.getTime() !== pr.updatedAt.getTime()) { + // Updated PR + await this.notifyHandlers({ + type: 'pr_updated', + pullRequest: pr, + previousState: cached, + }); + } + } + + // Update cache + this.prCache.data = newData; + this.prCache.lastModified = new Date(); + } catch (error) { + this.logger.error('Error polling pull requests', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Register event handler + */ + onEvent(handler: PollingEventHandler): () => void { + this.eventHandlers.add(handler); + return () => this.eventHandlers.delete(handler); + } + + /** + * Notify all handlers of an event + */ + private async notifyHandlers(event: PollingEvent): Promise { + const promises = Array.from(this.eventHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in polling handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Check if service is running + */ + getStatus(): { + running: boolean; + intervalMs: number; + lastPoll?: Date; + cachedIssues: number; + cachedPRs: number; + } { + return { + running: this.isRunning, + intervalMs: this.config.intervalMs, + lastPoll: this.issueCache.lastModified || this.prCache.lastModified, + cachedIssues: this.issueCache.data.size, + cachedPRs: this.prCache.data.size, + }; + } + + /** + * Force an immediate poll + */ + async pollNow(): Promise { + if (!this.isRunning) { + throw new Error('Polling service is not running'); + } + + await this.poll(); + } + + /** + * Clear cache + */ + clearCache(): void { + this.issueCache = { data: new Map() }; + this.prCache = { data: new Map() }; + this.logger.info('Cache cleared'); + } +} diff --git a/packages/mcp-server/src/github/SyncManager.ts b/packages/mcp-server/src/github/SyncManager.ts new file mode 100644 index 0000000..39863ee --- /dev/null +++ b/packages/mcp-server/src/github/SyncManager.ts @@ -0,0 +1,401 @@ +/** + * SyncManager - Orchestrate webhook + polling, deduplicate events, map issues β†’ tasks + */ + +import { v4 as uuidv4 } from 'uuid'; +import type { GitHubClient, GitHubWebhookEvent, GitHubIssue } from './GitHubClient.js'; +import type { WebhookHandler } from './WebhookHandler.js'; +import type { PollingService, PollingEvent } from './PollingService.js'; +import type { Task, TaskCreation } from '../tasks/Task.js'; +import { createTask } from '../tasks/Task.js'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Sync event (deduplicated GitHub event β†’ task creation) + */ +export interface SyncEvent { + /** Event type */ + type: 'task_created' | 'task_updated' | 'task_closed'; + + /** Created/updated task */ + task: Task; + + /** Original GitHub issue */ + issue: GitHubIssue; + + /** Event source */ + source: 'webhook' | 'polling'; +} + +/** + * Sync event handler + */ +export type SyncEventHandler = (event: SyncEvent) => void | Promise; + +/** + * SyncManager configuration + */ +export interface SyncManagerConfig { + /** GitHub client */ + client: GitHubClient; + + /** Webhook handler (optional) */ + webhookHandler?: WebhookHandler; + + /** Polling service (optional) */ + pollingService?: PollingService; + + /** Logger */ + logger?: Logger; + + /** Deduplication window in milliseconds */ + deduplicationWindowMs?: number; +} + +/** + * Event deduplication entry + */ +interface DeduplicationEntry { + issueNumber: number; + action: string; + timestamp: Date; +} + +/** + * SyncManager class + */ +export class SyncManager { + private client: GitHubClient; + private webhookHandler?: WebhookHandler; + private pollingService?: PollingService; + private logger: Logger; + private eventHandlers: Set; + private isRunning: boolean; + private deduplicationWindowMs: number; + private recentEvents: DeduplicationEntry[]; + private taskCache: Map; // issueNumber β†’ Task + + constructor(config: SyncManagerConfig) { + this.client = config.client; + this.webhookHandler = config.webhookHandler; + this.pollingService = config.pollingService; + this.logger = config.logger || createLogger(); + this.eventHandlers = new Set(); + this.isRunning = false; + this.deduplicationWindowMs = config.deduplicationWindowMs || 5000; // 5 seconds + this.recentEvents = []; + this.taskCache = new Map(); + } + + /** + * Start sync manager + */ + async start(): Promise { + if (this.isRunning) { + this.logger.warn('SyncManager already running'); + return; + } + + this.isRunning = true; + + // Start webhook handler if configured + if (this.webhookHandler) { + this.webhookHandler.onEvent(this.handleWebhookEvent.bind(this)); + if (!this.webhookHandler.isRunning()) { + await this.webhookHandler.start(); + } + this.logger.info('Webhook handler attached'); + } + + // Start polling service if configured + if (this.pollingService) { + this.pollingService.onEvent(this.handlePollingEvent.bind(this)); + this.pollingService.start(); + this.logger.info('Polling service attached'); + } + + // Start deduplication cleanup timer + this.startDeduplicationCleanup(); + + this.logger.info('SyncManager started'); + } + + /** + * Stop sync manager + */ + async stop(): Promise { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + + // Stop webhook handler + if (this.webhookHandler?.isRunning()) { + await this.webhookHandler.stop(); + } + + // Stop polling service + if (this.pollingService) { + this.pollingService.stop(); + } + + this.logger.info('SyncManager stopped'); + } + + /** + * Handle webhook event + */ + private async handleWebhookEvent(event: GitHubWebhookEvent): Promise { + try { + if (event.type !== 'issues' || !event.issue) { + return; // Only handle issue events + } + + const action = event.action || 'unknown'; + const issue = event.issue; + + // Check for duplicates + if (this.isDuplicate(issue.number, action, 'webhook')) { + this.logger.debug('Duplicate webhook event ignored', { + issueNumber: issue.number, + action, + }); + return; + } + + // Process event + await this.processIssueEvent(issue, action, 'webhook'); + } catch (error) { + this.logger.error('Error handling webhook event', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Handle polling event + */ + private async handlePollingEvent(event: PollingEvent): Promise { + try { + if (!event.issue) { + return; // Only handle issue events + } + + const action = event.type === 'new_issue' ? 'opened' : 'updated'; + const issue = event.issue; + + // Check for duplicates + if (this.isDuplicate(issue.number, action, 'polling')) { + this.logger.debug('Duplicate polling event ignored', { + issueNumber: issue.number, + action, + }); + return; + } + + // Process event + await this.processIssueEvent(issue, action, 'polling'); + } catch (error) { + this.logger.error('Error handling polling event', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + + /** + * Process issue event + */ + private async processIssueEvent( + issue: GitHubIssue, + action: string, + source: 'webhook' | 'polling' + ): Promise { + // Map to sync event type + let syncType: SyncEvent['type']; + if (action === 'opened') { + syncType = 'task_created'; + } else if (action === 'closed') { + syncType = 'task_closed'; + } else { + syncType = 'task_updated'; + } + + // Get or create task + let task = this.taskCache.get(issue.number); + + if (!task && syncType === 'task_created') { + // Create new task from issue + task = this.issueToTask(issue); + this.taskCache.set(issue.number, task); + } else if (task) { + // Update existing task from issue + task = this.updateTaskFromIssue(task, issue, action); + this.taskCache.set(issue.number, task); + } else { + // Task not in cache but event is not 'opened' - fetch from issue + task = this.issueToTask(issue); + this.taskCache.set(issue.number, task); + syncType = 'task_created'; // Treat as new + } + + // Notify handlers + await this.notifyHandlers({ + type: syncType, + task, + issue, + source, + }); + + this.logger.info('Synced issue to task', { + issueNumber: issue.number, + taskId: task.id, + action, + source, + }); + } + + /** + * Map GitHub issue to Task + */ + private issueToTask(issue: GitHubIssue): Task { + const taskData: TaskCreation = { + description: issue.title, + dependencies: [], + githubIssueId: String(issue.number), + githubIssueUrl: issue.htmlUrl, + }; + + return createTask(uuidv4(), taskData); + } + + /** + * Update task from issue + */ + private updateTaskFromIssue(task: Task, issue: GitHubIssue, action: string): Task { + // Update description if title changed + if (task.description !== issue.title) { + task = { ...task, description: issue.title }; + } + + // Update status based on issue state + if (action === 'closed' && issue.state === 'closed') { + task = { ...task, completedAt: new Date() }; + } + + return task; + } + + /** + * Check if event is a duplicate + */ + private isDuplicate( + issueNumber: number, + action: string, + source: 'webhook' | 'polling' + ): boolean { + const now = new Date(); + const cutoff = new Date(now.getTime() - this.deduplicationWindowMs); + + // Check recent events + const isDup = this.recentEvents.some( + (entry) => + entry.issueNumber === issueNumber && + entry.action === action && + entry.timestamp > cutoff + ); + + if (!isDup) { + // Record this event + this.recentEvents.push({ + issueNumber, + action, + timestamp: now, + }); + } + + return isDup; + } + + /** + * Start deduplication cleanup timer + */ + private startDeduplicationCleanup(): void { + setInterval(() => { + const now = new Date(); + const cutoff = new Date(now.getTime() - this.deduplicationWindowMs); + + // Remove old entries + this.recentEvents = this.recentEvents.filter( + (entry) => entry.timestamp > cutoff + ); + }, this.deduplicationWindowMs); + } + + /** + * Register sync event handler + */ + onSync(handler: SyncEventHandler): () => void { + this.eventHandlers.add(handler); + return () => this.eventHandlers.delete(handler); + } + + /** + * Notify all handlers + */ + private async notifyHandlers(event: SyncEvent): Promise { + const promises = Array.from(this.eventHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in sync handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Get task by GitHub issue number + */ + getTaskByIssueNumber(issueNumber: number): Task | undefined { + return this.taskCache.get(issueNumber); + } + + /** + * Get all synced tasks + */ + getAllTasks(): Task[] { + return Array.from(this.taskCache.values()); + } + + /** + * Clear task cache + */ + clearCache(): void { + this.taskCache.clear(); + this.logger.info('Task cache cleared'); + } + + /** + * Get sync status + */ + getStatus(): { + running: boolean; + webhookEnabled: boolean; + pollingEnabled: boolean; + cachedTasks: number; + recentEvents: number; + } { + return { + running: this.isRunning, + webhookEnabled: this.webhookHandler !== undefined, + pollingEnabled: this.pollingService !== undefined, + cachedTasks: this.taskCache.size, + recentEvents: this.recentEvents.length, + }; + } +} diff --git a/packages/mcp-server/src/github/WebhookHandler.ts b/packages/mcp-server/src/github/WebhookHandler.ts new file mode 100644 index 0000000..6af7b11 --- /dev/null +++ b/packages/mcp-server/src/github/WebhookHandler.ts @@ -0,0 +1,340 @@ +/** + * WebhookHandler - Express endpoint for GitHub webhook events + * Validates webhook signatures and parses issue/PR events + */ + +import express, { Request, Response, Application } from 'express'; +import { createHmac, timingSafeEqual } from 'crypto'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; +import { GitHubWebhookEvent, GitHubIssue, GitHubPullRequest } from './GitHubClient.js'; + +/** + * Webhook event handler callback + */ +export type WebhookEventHandler = (event: GitHubWebhookEvent) => void | Promise; + +/** + * Webhook handler configuration + */ +export interface WebhookHandlerConfig { + /** Webhook secret for signature validation */ + secret?: string; + + /** Port to listen on */ + port?: number; + + /** Path for webhook endpoint */ + path?: string; + + /** Logger instance */ + logger?: Logger; + + /** Whether to verify signatures */ + verifySignature?: boolean; +} + +/** + * WebhookHandler class + */ +export class WebhookHandler { + private app: Application; + private config: Required; + private logger: Logger; + private eventHandlers: Set; + private server?: ReturnType; + + constructor(config: WebhookHandlerConfig = {}) { + this.config = { + secret: config.secret || '', + port: config.port || 3000, + path: config.path || '/webhook', + logger: config.logger || createLogger(), + verifySignature: config.verifySignature ?? true, + }; + this.logger = this.config.logger; + this.eventHandlers = new Set(); + this.app = express(); + this.setupMiddleware(); + this.setupRoutes(); + } + + /** + * Setup Express middleware + */ + private setupMiddleware(): void { + // Raw body parser for signature verification + this.app.use( + express.json({ + verify: (req: any, res, buf) => { + req.rawBody = buf.toString('utf-8'); + }, + }) + ); + } + + /** + * Setup Express routes + */ + private setupRoutes(): void { + // Health check endpoint + this.app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); + }); + + // Webhook endpoint + this.app.post(this.config.path, this.handleWebhook.bind(this)); + } + + /** + * Handle incoming webhook + */ + private async handleWebhook(req: Request, res: Response): Promise { + try { + // Verify signature if enabled + if (this.config.verifySignature) { + const isValid = this.verifySignature( + req.headers['x-hub-signature-256'] as string, + (req as any).rawBody + ); + + if (!isValid) { + this.logger.warn('Invalid webhook signature'); + res.status(401).json({ error: 'Invalid signature' }); + return; + } + } + + // Get event type from header + const eventType = req.headers['x-github-event'] as string; + const deliveryId = req.headers['x-github-delivery'] as string; + + this.logger.debug('Received webhook event', { + eventType, + deliveryId, + }); + + // Parse event + const event = this.parseEvent(eventType, req.body); + + // Notify handlers + await this.notifyHandlers(event); + + res.status(200).json({ received: true }); + } catch (error) { + this.logger.error('Error handling webhook', { + error: error instanceof Error ? error : new Error(String(error)), + }); + res.status(500).json({ error: 'Internal server error' }); + } + } + + /** + * Verify webhook signature + */ + private verifySignature(signature: string | undefined, body: string): boolean { + if (!signature || !this.config.secret) { + return !this.config.verifySignature; // Skip verification if no secret configured + } + + try { + const hmac = createHmac('sha256', this.config.secret); + hmac.update(body); + const expectedSignature = `sha256=${hmac.digest('hex')}`; + + // Use timing-safe comparison + return timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } catch (error) { + this.logger.error('Error verifying signature', { + error: error instanceof Error ? error : new Error(String(error)), + }); + return false; + } + } + + /** + * Parse webhook event + */ + private parseEvent(eventType: string, payload: any): GitHubWebhookEvent { + switch (eventType) { + case 'issues': + return this.parseIssueEvent(payload); + + case 'pull_request': + return this.parsePullRequestEvent(payload); + + case 'push': + return this.parsePushEvent(payload); + + default: + return { + type: 'unknown', + repository: { + owner: payload.repository?.owner?.login || 'unknown', + name: payload.repository?.name || 'unknown', + }, + }; + } + } + + /** + * Parse issue event + */ + private parseIssueEvent(payload: any): GitHubWebhookEvent { + const issue: GitHubIssue = { + id: payload.issue.id, + number: payload.issue.number, + title: payload.issue.title, + body: payload.issue.body, + state: payload.issue.state, + url: payload.issue.url, + htmlUrl: payload.issue.html_url, + createdAt: new Date(payload.issue.created_at), + updatedAt: new Date(payload.issue.updated_at), + labels: payload.issue.labels.map((label: any) => label.name), + assignees: payload.issue.assignees.map((assignee: any) => assignee.login), + }; + + return { + type: 'issues', + action: payload.action, + issue, + repository: { + owner: payload.repository.owner.login, + name: payload.repository.name, + }, + }; + } + + /** + * Parse pull request event + */ + private parsePullRequestEvent(payload: any): GitHubWebhookEvent { + const pullRequest: GitHubPullRequest = { + id: payload.pull_request.id, + number: payload.pull_request.number, + title: payload.pull_request.title, + body: payload.pull_request.body, + state: payload.pull_request.state, + url: payload.pull_request.url, + htmlUrl: payload.pull_request.html_url, + createdAt: new Date(payload.pull_request.created_at), + updatedAt: new Date(payload.pull_request.updated_at), + head: payload.pull_request.head.ref, + base: payload.pull_request.base.ref, + mergeable: payload.pull_request.mergeable, + }; + + return { + type: 'pull_request', + action: payload.action, + pullRequest, + repository: { + owner: payload.repository.owner.login, + name: payload.repository.name, + }, + }; + } + + /** + * Parse push event + */ + private parsePushEvent(payload: any): GitHubWebhookEvent { + return { + type: 'push', + action: 'pushed', + repository: { + owner: payload.repository.owner.login, + name: payload.repository.name, + }, + }; + } + + /** + * Register event handler + */ + onEvent(handler: WebhookEventHandler): () => void { + this.eventHandlers.add(handler); + return () => this.eventHandlers.delete(handler); + } + + /** + * Notify all handlers of an event + */ + private async notifyHandlers(event: GitHubWebhookEvent): Promise { + const promises = Array.from(this.eventHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in webhook handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Start the webhook server + */ + async start(): Promise { + return new Promise((resolve, reject) => { + try { + this.server = this.app.listen(this.config.port, () => { + this.logger.info('Webhook server started', { + port: this.config.port, + path: this.config.path, + }); + resolve(); + }); + + this.server.on('error', reject); + } catch (error) { + reject(error); + } + }); + } + + /** + * Stop the webhook server + */ + async stop(): Promise { + return new Promise((resolve, reject) => { + if (!this.server) { + resolve(); + return; + } + + this.server.close((error) => { + if (error) { + reject(error); + } else { + this.logger.info('Webhook server stopped'); + this.server = undefined; + resolve(); + } + }); + }); + } + + /** + * Check if server is running + */ + isRunning(): boolean { + return this.server !== undefined; + } + + /** + * Get server configuration + */ + getConfig(): { port: number; path: string } { + return { + port: this.config.port, + path: this.config.path, + }; + } +} diff --git a/packages/mcp-server/src/logging/LogFormatter.ts b/packages/mcp-server/src/logging/LogFormatter.ts new file mode 100644 index 0000000..0b4ded5 --- /dev/null +++ b/packages/mcp-server/src/logging/LogFormatter.ts @@ -0,0 +1,330 @@ +/** + * LogFormatter - Structured JSON logging formatter + * Formats log entries for output to console, files, or external logging services + */ + +import { LogEntry, LogLevel, LogMetadata } from './Logger.js'; + +/** + * Formatting options + */ +export interface FormatOptions { + /** Output format */ + format: 'json' | 'text'; + + /** Whether to colorize output (text format only) */ + colorize?: boolean; + + /** Whether to include timestamp */ + includeTimestamp?: boolean; + + /** Whether to pretty-print JSON */ + prettyPrint?: boolean; + + /** Indent size for pretty-printing */ + indent?: number; + + /** Maximum metadata depth to include */ + maxDepth?: number; +} + +/** + * ANSI color codes for console output + */ +const Colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + gray: '\x1b[90m', +}; + +/** + * Log level colors + */ +const LevelColors: Record = { + [LogLevel.ERROR]: Colors.red, + [LogLevel.WARN]: Colors.yellow, + [LogLevel.INFO]: Colors.blue, + [LogLevel.DEBUG]: Colors.gray, +}; + +/** + * LogFormatter class + */ +export class LogFormatter { + private options: Required; + + constructor(options: Partial = {}) { + this.options = { + format: 'json', + colorize: false, + includeTimestamp: true, + prettyPrint: false, + indent: 2, + maxDepth: 5, + ...options, + }; + } + + /** + * Format a log entry + */ + format(entry: LogEntry): string { + switch (this.options.format) { + case 'json': + return this.formatJSON(entry); + case 'text': + return this.formatText(entry); + default: + return this.formatJSON(entry); + } + } + + /** + * Format as JSON + */ + private formatJSON(entry: LogEntry): string { + const obj = this.prepareEntry(entry); + + if (this.options.prettyPrint) { + return JSON.stringify(obj, this.getReplacer(), this.options.indent); + } + + return JSON.stringify(obj, this.getReplacer()); + } + + /** + * Format as human-readable text + */ + private formatText(entry: LogEntry): string { + const parts: string[] = []; + + // Timestamp + if (this.options.includeTimestamp) { + const timestamp = this.formatTimestamp(entry.timestamp); + parts.push(this.colorize(timestamp, Colors.gray)); + } + + // Level + const level = this.formatLevel(entry.level); + parts.push(level); + + // Message + parts.push(entry.message); + + // Metadata + if (entry.metadata && Object.keys(entry.metadata).length > 0) { + const metadata = this.formatMetadata(entry.metadata); + parts.push(this.colorize(metadata, Colors.dim)); + } + + return parts.join(' '); + } + + /** + * Prepare entry for serialization + */ + private prepareEntry(entry: LogEntry): Record { + const obj: Record = { + level: entry.level, + message: entry.message, + }; + + if (this.options.includeTimestamp) { + obj.timestamp = entry.timestamp; + } + + if (entry.metadata) { + // Flatten metadata into top level or keep nested + obj.metadata = this.sanitizeMetadata(entry.metadata); + } + + return obj; + } + + /** + * Sanitize metadata (handle errors, circular references, depth) + */ + private sanitizeMetadata( + metadata: LogMetadata, + depth: number = 0 + ): Record { + if (depth >= this.options.maxDepth) { + return { __truncated: true }; + } + + const sanitized: Record = {}; + + for (const [key, value] of Object.entries(metadata)) { + if (value instanceof Error) { + sanitized[key] = { + message: value.message, + name: value.name, + stack: value.stack, + }; + } else if (value instanceof Date) { + sanitized[key] = value.toISOString(); + } else if (Array.isArray(value)) { + sanitized[key] = value.map((item) => + typeof item === 'object' && item !== null + ? this.sanitizeMetadata(item as LogMetadata, depth + 1) + : item + ); + } else if (typeof value === 'object' && value !== null) { + sanitized[key] = this.sanitizeMetadata(value as LogMetadata, depth + 1); + } else { + sanitized[key] = value; + } + } + + return sanitized; + } + + /** + * Get JSON replacer function + */ + private getReplacer(): (key: string, value: unknown) => unknown { + const seen = new WeakSet(); + + return (key: string, value: unknown) => { + // Handle circular references + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + + // Handle special types + if (value instanceof Error) { + return { + message: value.message, + name: value.name, + stack: value.stack, + }; + } + + if (value instanceof Date) { + return value.toISOString(); + } + + return value; + }; + } + + /** + * Format timestamp for text output + */ + private formatTimestamp(timestamp: string): string { + const date = new Date(timestamp); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const ms = String(date.getMilliseconds()).padStart(3, '0'); + return `${hours}:${minutes}:${seconds}.${ms}`; + } + + /** + * Format log level for text output + */ + private formatLevel(level: LogLevel): string { + const formatted = `[${level}]`.padEnd(7); + return this.colorize(formatted, LevelColors[level]); + } + + /** + * Format metadata for text output + */ + private formatMetadata(metadata: LogMetadata): string { + const parts: string[] = []; + + // Common fields first + if (metadata.component) { + parts.push(`component=${metadata.component}`); + } + if (metadata.agentId) { + parts.push(`agent=${metadata.agentId.slice(0, 8)}`); + } + if (metadata.taskId) { + parts.push(`task=${metadata.taskId.slice(0, 8)}`); + } + if (metadata.duration !== undefined) { + parts.push(`duration=${metadata.duration}ms`); + } + + // Other fields + for (const [key, value] of Object.entries(metadata)) { + if (!['component', 'agentId', 'taskId', 'duration', 'error'].includes(key)) { + parts.push(`${key}=${this.stringifyValue(value)}`); + } + } + + // Error last + if (metadata.error instanceof Error) { + parts.push(`error="${metadata.error.message}"`); + } + + return parts.length > 0 ? `{${parts.join(', ')}}` : ''; + } + + /** + * Stringify a value for text output + */ + private stringifyValue(value: unknown): string { + if (typeof value === 'string') { + return `"${value}"`; + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return String(value); + } + + /** + * Apply color if colorize is enabled + */ + private colorize(text: string, color: string): string { + if (!this.options.colorize) { + return text; + } + return `${color}${text}${Colors.reset}`; + } + + /** + * Update formatter options + */ + setOptions(options: Partial): void { + this.options = { ...this.options, ...options }; + } + + /** + * Get current options + */ + getOptions(): FormatOptions { + return { ...this.options }; + } +} + +/** + * Create a formatter instance + */ +export function createFormatter(options?: Partial): LogFormatter { + return new LogFormatter(options); +} + +/** + * Default formatters + */ +export const jsonFormatter = createFormatter({ format: 'json' }); +export const prettyJsonFormatter = createFormatter({ + format: 'json', + prettyPrint: true, +}); +export const textFormatter = createFormatter({ + format: 'text', + colorize: true, +}); diff --git a/packages/mcp-server/src/logging/Logger.ts b/packages/mcp-server/src/logging/Logger.ts new file mode 100644 index 0000000..7392da9 --- /dev/null +++ b/packages/mcp-server/src/logging/Logger.ts @@ -0,0 +1,268 @@ +/** + * Logger - Structured logging interface with multiple levels + * Based on specs/001-multi-agent-coordination/plan.md logging requirements + */ + +/** + * Log levels in order of severity + */ +export enum LogLevel { + ERROR = 'ERROR', + WARN = 'WARN', + INFO = 'INFO', + DEBUG = 'DEBUG', +} + +/** + * Log level numeric values for comparison + */ +export const LogLevelValue: Record = { + [LogLevel.ERROR]: 40, + [LogLevel.WARN]: 30, + [LogLevel.INFO]: 20, + [LogLevel.DEBUG]: 10, +}; + +/** + * Log entry metadata + */ +export interface LogMetadata { + /** Component or module name */ + component?: string; + + /** Agent ID if applicable */ + agentId?: string; + + /** Task ID if applicable */ + taskId?: string; + + /** Request correlation ID */ + correlationId?: string; + + /** Error object */ + error?: Error; + + /** Duration in milliseconds */ + duration?: number; + + /** Custom key-value pairs */ + [key: string]: unknown; +} + +/** + * Log entry structure + */ +export interface LogEntry { + /** Log level */ + level: LogLevel; + + /** Log message */ + message: string; + + /** Timestamp (ISO 8601) */ + timestamp: string; + + /** Additional metadata */ + metadata?: LogMetadata; +} + +/** + * Logger interface + */ +export interface Logger { + /** + * Get current log level + */ + readonly level: LogLevel; + + /** + * Set log level + */ + setLevel(level: LogLevel): void; + + /** + * Log an error message + */ + error(message: string, metadata?: LogMetadata): void; + + /** + * Log a warning message + */ + warn(message: string, metadata?: LogMetadata): void; + + /** + * Log an info message + */ + info(message: string, metadata?: LogMetadata): void; + + /** + * Log a debug message + */ + debug(message: string, metadata?: LogMetadata): void; + + /** + * Log at a specific level + */ + log(level: LogLevel, message: string, metadata?: LogMetadata): void; + + /** + * Check if a log level is enabled + */ + isLevelEnabled(level: LogLevel): boolean; + + /** + * Create a child logger with preset metadata + */ + child(metadata: LogMetadata): Logger; +} + +/** + * Base logger implementation + */ +export abstract class BaseLogger implements Logger { + protected _level: LogLevel; + protected childMetadata?: LogMetadata; + + constructor(level: LogLevel = LogLevel.INFO, childMetadata?: LogMetadata) { + this._level = level; + this.childMetadata = childMetadata; + } + + get level(): LogLevel { + return this._level; + } + + setLevel(level: LogLevel): void { + this._level = level; + } + + error(message: string, metadata?: LogMetadata): void { + this.log(LogLevel.ERROR, message, metadata); + } + + warn(message: string, metadata?: LogMetadata): void { + this.log(LogLevel.WARN, message, metadata); + } + + info(message: string, metadata?: LogMetadata): void { + this.log(LogLevel.INFO, message, metadata); + } + + debug(message: string, metadata?: LogMetadata): void { + this.log(LogLevel.DEBUG, message, metadata); + } + + log(level: LogLevel, message: string, metadata?: LogMetadata): void { + if (!this.isLevelEnabled(level)) { + return; + } + + const entry: LogEntry = { + level, + message, + timestamp: new Date().toISOString(), + metadata: this.mergeMetadata(metadata), + }; + + this.write(entry); + } + + isLevelEnabled(level: LogLevel): boolean { + return LogLevelValue[level] >= LogLevelValue[this._level]; + } + + child(metadata: LogMetadata): Logger { + const childMeta = this.mergeMetadata(metadata); + return new (this.constructor as new ( + level: LogLevel, + childMetadata?: LogMetadata + ) => BaseLogger)(this._level, childMeta); + } + + /** + * Merge child metadata with provided metadata + */ + protected mergeMetadata(metadata?: LogMetadata): LogMetadata | undefined { + if (!this.childMetadata && !metadata) { + return undefined; + } + + return { + ...this.childMetadata, + ...metadata, + }; + } + + /** + * Abstract method to write log entry (implemented by subclasses) + */ + protected abstract write(entry: LogEntry): void; +} + +/** + * Console logger implementation + */ +export class ConsoleLogger extends BaseLogger { + protected write(entry: LogEntry): void { + const method = this.getConsoleMethod(entry.level); + method(this.format(entry)); + } + + /** + * Get appropriate console method for log level + */ + private getConsoleMethod( + level: LogLevel + ): (...args: unknown[]) => void { + switch (level) { + case LogLevel.ERROR: + return console.error.bind(console); + case LogLevel.WARN: + return console.warn.bind(console); + case LogLevel.INFO: + return console.info.bind(console); + case LogLevel.DEBUG: + return console.debug.bind(console); + default: + return console.log.bind(console); + } + } + + /** + * Format log entry (can be overridden) + */ + protected format(entry: LogEntry): string { + return JSON.stringify(entry); + } +} + +/** + * No-op logger (discards all logs) + */ +export class NoopLogger extends BaseLogger { + protected write(_entry: LogEntry): void { + // No-op + } +} + +/** + * Create a logger instance + */ +export function createLogger( + level: LogLevel = LogLevel.INFO, + type: 'console' | 'noop' = 'console' +): Logger { + switch (type) { + case 'console': + return new ConsoleLogger(level); + case 'noop': + return new NoopLogger(level); + default: + return new ConsoleLogger(level); + } +} + +/** + * Default logger instance + */ +export const logger = createLogger(); diff --git a/packages/mcp-server/src/protocol/Message.ts b/packages/mcp-server/src/protocol/Message.ts new file mode 100644 index 0000000..4f65bbf --- /dev/null +++ b/packages/mcp-server/src/protocol/Message.ts @@ -0,0 +1,207 @@ +/** + * CoorChat Message Protocol - TypeScript Type Definitions + * Based on message-protocol.json schema v1.0 + */ + +/** + * Message types for agent coordination + */ +export enum MessageType { + TASK_ASSIGNED = 'task_assigned', + TASK_STARTED = 'task_started', + TASK_BLOCKED = 'task_blocked', + TASK_PROGRESS = 'task_progress', + TASK_COMPLETED = 'task_completed', + TASK_FAILED = 'task_failed', + CAPABILITY_QUERY = 'capability_query', + CAPABILITY_RESPONSE = 'capability_response', + STATUS_QUERY = 'status_query', + STATUS_RESPONSE = 'status_response', + ERROR = 'error', + HEARTBEAT = 'heartbeat', + AGENT_JOINED = 'agent_joined', + AGENT_LEFT = 'agent_left', +} + +/** + * Delivery status states + */ +export enum DeliveryStatus { + QUEUED = 'queued', + SENDING = 'sending', + SENT = 'sent', + DELIVERED = 'delivered', + ACKNOWLEDGED = 'acknowledged', + FAILED = 'failed', +} + +/** + * Resource limits for agent capabilities + */ +export interface ResourceLimits { + apiQuotaPerHour?: number; + maxConcurrentTasks?: number; + rateLimitPerMinute?: number; + memoryLimitMB?: number; +} + +/** + * Payload for task_assigned messages + */ +export interface TaskAssignedPayload { + taskId: string; + description: string; + dependencies?: string[]; + githubIssue: string; +} + +/** + * Payload for task_progress messages + */ +export interface TaskProgressPayload { + taskId: string; + percentComplete: number; + status: string; +} + +/** + * Payload for task_completed messages + */ +export interface TaskCompletedPayload { + taskId: string; + result: Record; + githubPR?: string | null; +} + +/** + * Payload for task_failed messages + */ +export interface TaskFailedPayload { + taskId: string; + error: string; + retryable: boolean; + stackTrace?: string | null; +} + +/** + * Payload for capability_response messages + */ +export interface CapabilityResponsePayload { + agentId: string; + roleType: string; + platform: string; + environmentType?: string; + tools: string[]; + languages?: string[]; + apiAccess?: string[]; + resourceLimits?: ResourceLimits; +} + +/** + * Payload for error messages + */ +export interface ErrorPayload { + code: string; + message: string; + details?: Record | null; +} + +/** + * Union type of all possible payloads + */ +export type MessagePayload = + | TaskAssignedPayload + | TaskProgressPayload + | TaskCompletedPayload + | TaskFailedPayload + | CapabilityResponsePayload + | ErrorPayload + | Record; + +/** + * Core message structure for all agent communications + */ +export interface Message { + /** Semantic version of the protocol (e.g., '1.0') */ + protocolVersion: string; + + /** Type of message being sent */ + messageType: MessageType; + + /** UUID of the sending agent */ + senderId: string; + + /** UUID of the recipient agent (null for broadcast) */ + recipientId?: string | null; + + /** UUID of the associated task (if applicable) */ + taskId?: string | null; + + /** Message priority (0=lowest, 10=highest) */ + priority?: number; + + /** ISO 8601 timestamp when message was created */ + timestamp: string; + + /** UUID for matching request/response pairs */ + correlationId?: string | null; + + /** Message-specific data (varies by messageType) */ + payload?: MessagePayload; + + /** Current delivery state of the message */ + deliveryStatus?: DeliveryStatus; +} + +/** + * Type guard to check if a value is a valid Message + */ +export function isMessage(value: unknown): value is Message { + if (typeof value !== 'object' || value === null) { + return false; + } + + const msg = value as Partial; + + return ( + typeof msg.protocolVersion === 'string' && + typeof msg.messageType === 'string' && + Object.values(MessageType).includes(msg.messageType as MessageType) && + typeof msg.senderId === 'string' && + typeof msg.timestamp === 'string' + ); +} + +/** + * Type guard to check if a message type is task-related + */ +export function isTaskMessage(messageType: MessageType): boolean { + return [ + MessageType.TASK_ASSIGNED, + MessageType.TASK_STARTED, + MessageType.TASK_BLOCKED, + MessageType.TASK_PROGRESS, + MessageType.TASK_COMPLETED, + MessageType.TASK_FAILED, + ].includes(messageType); +} + +/** + * Type guard to check if a message type is capability-related + */ +export function isCapabilityMessage(messageType: MessageType): boolean { + return [ + MessageType.CAPABILITY_QUERY, + MessageType.CAPABILITY_RESPONSE, + ].includes(messageType); +} + +/** + * Type guard to check if a message type is status-related + */ +export function isStatusMessage(messageType: MessageType): boolean { + return [ + MessageType.STATUS_QUERY, + MessageType.STATUS_RESPONSE, + ].includes(messageType); +} diff --git a/packages/mcp-server/src/protocol/MessageBuilder.ts b/packages/mcp-server/src/protocol/MessageBuilder.ts new file mode 100644 index 0000000..71dbd2f --- /dev/null +++ b/packages/mcp-server/src/protocol/MessageBuilder.ts @@ -0,0 +1,353 @@ +/** + * MessageBuilder - Fluent API for constructing CoorChat messages + */ + +import { v4 as uuidv4 } from 'uuid'; +import { + Message, + MessageType, + DeliveryStatus, + MessagePayload, + TaskAssignedPayload, + TaskProgressPayload, + TaskCompletedPayload, + TaskFailedPayload, + CapabilityResponsePayload, + ErrorPayload, +} from './Message.js'; + +/** + * Default protocol version + */ +const DEFAULT_PROTOCOL_VERSION = '1.0'; + +/** + * Default message priority + */ +const DEFAULT_PRIORITY = 5; + +/** + * Fluent builder for creating Message instances + */ +export class MessageBuilder { + private message: Partial; + + constructor() { + this.message = { + protocolVersion: DEFAULT_PROTOCOL_VERSION, + priority: DEFAULT_PRIORITY, + timestamp: new Date().toISOString(), + deliveryStatus: DeliveryStatus.QUEUED, + }; + } + + /** + * Set the message type + */ + type(messageType: MessageType): this { + this.message.messageType = messageType; + return this; + } + + /** + * Set the sender ID + */ + from(senderId: string): this { + this.message.senderId = senderId; + return this; + } + + /** + * Set the recipient ID (null or undefined for broadcast) + */ + to(recipientId: string | null | undefined): this { + this.message.recipientId = recipientId; + return this; + } + + /** + * Set the task ID + */ + forTask(taskId: string | null | undefined): this { + this.message.taskId = taskId; + return this; + } + + /** + * Set the message priority (0-10) + */ + priority(priority: number): this { + this.message.priority = Math.max(0, Math.min(10, priority)); + return this; + } + + /** + * Set the correlation ID for request/response matching + */ + correlate(correlationId: string | null | undefined): this { + this.message.correlationId = correlationId; + return this; + } + + /** + * Set the protocol version + */ + version(protocolVersion: string): this { + this.message.protocolVersion = protocolVersion; + return this; + } + + /** + * Set the message payload + */ + payload(payload: MessagePayload): this { + this.message.payload = payload; + return this; + } + + /** + * Set the delivery status + */ + status(deliveryStatus: DeliveryStatus): this { + this.message.deliveryStatus = deliveryStatus; + return this; + } + + /** + * Build and return the completed message + * @throws Error if required fields are missing + */ + build(): Message { + if (!this.message.messageType) { + throw new Error('Message type is required'); + } + if (!this.message.senderId) { + throw new Error('Sender ID is required'); + } + + return this.message as Message; + } + + /** + * Create a task_assigned message + */ + static taskAssigned( + senderId: string, + recipientId: string, + payload: TaskAssignedPayload + ): Message { + return new MessageBuilder() + .type(MessageType.TASK_ASSIGNED) + .from(senderId) + .to(recipientId) + .forTask(payload.taskId) + .priority(7) + .payload(payload) + .build(); + } + + /** + * Create a task_started message + */ + static taskStarted( + senderId: string, + taskId: string, + payload?: Record + ): Message { + return new MessageBuilder() + .type(MessageType.TASK_STARTED) + .from(senderId) + .forTask(taskId) + .payload(payload || {}) + .build(); + } + + /** + * Create a task_progress message + */ + static taskProgress( + senderId: string, + payload: TaskProgressPayload + ): Message { + return new MessageBuilder() + .type(MessageType.TASK_PROGRESS) + .from(senderId) + .forTask(payload.taskId) + .payload(payload) + .build(); + } + + /** + * Create a task_completed message + */ + static taskCompleted( + senderId: string, + payload: TaskCompletedPayload + ): Message { + return new MessageBuilder() + .type(MessageType.TASK_COMPLETED) + .from(senderId) + .forTask(payload.taskId) + .priority(8) + .payload(payload) + .build(); + } + + /** + * Create a task_failed message + */ + static taskFailed( + senderId: string, + payload: TaskFailedPayload + ): Message { + return new MessageBuilder() + .type(MessageType.TASK_FAILED) + .from(senderId) + .forTask(payload.taskId) + .priority(9) + .payload(payload) + .build(); + } + + /** + * Create a task_blocked message + */ + static taskBlocked( + senderId: string, + taskId: string, + blockedBy: string[], + reason: string + ): Message { + return new MessageBuilder() + .type(MessageType.TASK_BLOCKED) + .from(senderId) + .forTask(taskId) + .priority(7) + .payload({ blockedBy, reason }) + .build(); + } + + /** + * Create a capability_query message + */ + static capabilityQuery( + senderId: string, + recipientId?: string | null, + correlationId?: string + ): Message { + return new MessageBuilder() + .type(MessageType.CAPABILITY_QUERY) + .from(senderId) + .to(recipientId) + .correlate(correlationId || uuidv4()) + .payload({}) + .build(); + } + + /** + * Create a capability_response message + */ + static capabilityResponse( + senderId: string, + payload: CapabilityResponsePayload, + correlationId: string + ): Message { + return new MessageBuilder() + .type(MessageType.CAPABILITY_RESPONSE) + .from(senderId) + .correlate(correlationId) + .payload(payload) + .build(); + } + + /** + * Create a status_query message + */ + static statusQuery( + senderId: string, + recipientId?: string | null, + correlationId?: string + ): Message { + return new MessageBuilder() + .type(MessageType.STATUS_QUERY) + .from(senderId) + .to(recipientId) + .correlate(correlationId || uuidv4()) + .payload({}) + .build(); + } + + /** + * Create a status_response message + */ + static statusResponse( + senderId: string, + status: Record, + correlationId: string + ): Message { + return new MessageBuilder() + .type(MessageType.STATUS_RESPONSE) + .from(senderId) + .correlate(correlationId) + .payload(status) + .build(); + } + + /** + * Create an error message + */ + static error( + senderId: string, + payload: ErrorPayload, + priority: number = 9 + ): Message { + return new MessageBuilder() + .type(MessageType.ERROR) + .from(senderId) + .priority(priority) + .payload(payload) + .build(); + } + + /** + * Create a heartbeat message + */ + static heartbeat(senderId: string): Message { + return new MessageBuilder() + .type(MessageType.HEARTBEAT) + .from(senderId) + .priority(1) + .payload({ timestamp: new Date().toISOString() }) + .build(); + } + + /** + * Create an agent_joined message + */ + static agentJoined( + senderId: string, + agentInfo: Record + ): Message { + return new MessageBuilder() + .type(MessageType.AGENT_JOINED) + .from(senderId) + .priority(6) + .payload(agentInfo) + .build(); + } + + /** + * Create an agent_left message + */ + static agentLeft( + senderId: string, + reason?: string + ): Message { + return new MessageBuilder() + .type(MessageType.AGENT_LEFT) + .from(senderId) + .priority(6) + .payload({ reason: reason || 'Normal disconnect' }) + .build(); + } +} diff --git a/packages/mcp-server/src/protocol/MessageValidator.ts b/packages/mcp-server/src/protocol/MessageValidator.ts new file mode 100644 index 0000000..aeec4db --- /dev/null +++ b/packages/mcp-server/src/protocol/MessageValidator.ts @@ -0,0 +1,271 @@ +/** + * MessageValidator - JSON Schema-based validation for CoorChat messages + */ + +import Ajv, { ValidateFunction } from 'ajv'; +import addFormats from 'ajv-formats'; +import { Message, MessageType } from './Message.js'; + +/** + * Message protocol JSON schema + * Based on specs/001-multi-agent-coordination/contracts/message-protocol.json + */ +const messageSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://coorchat.dev/schemas/message-protocol/v1.0.json', + title: 'CoorChat Message Protocol', + description: 'JSON Schema for Multi-Agent Coordination System message format', + type: 'object', + required: ['protocolVersion', 'messageType', 'senderId', 'timestamp'], + properties: { + protocolVersion: { + type: 'string', + pattern: '^\\d+\\.\\d+$', + description: "Semantic version of the protocol (e.g., '1.0')", + }, + messageType: { + type: 'string', + enum: [ + 'task_assigned', + 'task_started', + 'task_blocked', + 'task_progress', + 'task_completed', + 'task_failed', + 'capability_query', + 'capability_response', + 'status_query', + 'status_response', + 'error', + 'heartbeat', + 'agent_joined', + 'agent_left', + ], + description: 'Type of message being sent', + }, + senderId: { + type: 'string', + format: 'uuid', + description: 'UUID of the sending agent', + }, + recipientId: { + type: ['string', 'null'], + format: 'uuid', + description: 'UUID of the recipient agent (null for broadcast)', + }, + taskId: { + type: ['string', 'null'], + format: 'uuid', + description: 'UUID of the associated task (if applicable)', + }, + priority: { + type: 'integer', + minimum: 0, + maximum: 10, + default: 5, + description: 'Message priority (0=lowest, 10=highest)', + }, + timestamp: { + type: 'string', + format: 'date-time', + description: 'ISO 8601 timestamp when message was created', + }, + correlationId: { + type: ['string', 'null'], + format: 'uuid', + description: 'UUID for matching request/response pairs', + }, + payload: { + type: 'object', + description: 'Message-specific data (varies by messageType)', + }, + deliveryStatus: { + type: 'string', + enum: ['queued', 'sending', 'sent', 'delivered', 'acknowledged', 'failed'], + default: 'queued', + description: 'Current delivery state of the message', + }, + }, +}; + +/** + * Validation error details + */ +export interface ValidationError { + field: string; + message: string; + value?: unknown; +} + +/** + * Validation result + */ +export interface ValidationResult { + valid: boolean; + errors?: ValidationError[]; +} + +/** + * MessageValidator class for validating protocol compliance + */ +export class MessageValidator { + private ajv: Ajv; + private validateMessage: ValidateFunction; + + constructor() { + this.ajv = new Ajv({ + allErrors: true, + strict: false, + validateFormats: true, + }); + + // Add format validators (uuid, date-time, uri, etc.) + addFormats(this.ajv); + + // Compile the message schema + this.validateMessage = this.ajv.compile(messageSchema); + } + + /** + * Validate a message against the protocol schema + */ + validate(message: unknown): ValidationResult { + const valid = this.validateMessage(message); + + if (valid) { + return { valid: true }; + } + + const errors: ValidationError[] = (this.validateMessage.errors || []).map( + (err) => ({ + field: err.instancePath || err.params?.missingProperty || 'unknown', + message: err.message || 'Validation failed', + value: err.data, + }) + ); + + return { valid: false, errors }; + } + + /** + * Validate and throw on error + * @throws Error if validation fails + */ + validateOrThrow(message: unknown): asserts message is Message { + const result = this.validate(message); + + if (!result.valid) { + const errorDetails = result.errors! + .map((err) => `${err.field}: ${err.message}`) + .join(', '); + throw new Error(`Message validation failed: ${errorDetails}`); + } + } + + /** + * Check if message type requires a taskId + */ + static requiresTaskId(messageType: MessageType): boolean { + return [ + MessageType.TASK_ASSIGNED, + MessageType.TASK_STARTED, + MessageType.TASK_BLOCKED, + MessageType.TASK_PROGRESS, + MessageType.TASK_COMPLETED, + MessageType.TASK_FAILED, + ].includes(messageType); + } + + /** + * Check if message type requires a correlationId + */ + static requiresCorrelationId(messageType: MessageType): boolean { + return [ + MessageType.CAPABILITY_RESPONSE, + MessageType.STATUS_RESPONSE, + ].includes(messageType); + } + + /** + * Validate message type-specific requirements + */ + validateSemantics(message: Message): ValidationResult { + const errors: ValidationError[] = []; + + // Check taskId requirement + if (MessageValidator.requiresTaskId(message.messageType) && !message.taskId) { + errors.push({ + field: 'taskId', + message: `Message type ${message.messageType} requires a taskId`, + }); + } + + // Check correlationId requirement + if ( + MessageValidator.requiresCorrelationId(message.messageType) && + !message.correlationId + ) { + errors.push({ + field: 'correlationId', + message: `Message type ${message.messageType} requires a correlationId`, + }); + } + + // Check priority range + if (message.priority !== undefined && (message.priority < 0 || message.priority > 10)) { + errors.push({ + field: 'priority', + message: 'Priority must be between 0 and 10', + value: message.priority, + }); + } + + // Validate protocol version format + if (!/^\d+\.\d+$/.test(message.protocolVersion)) { + errors.push({ + field: 'protocolVersion', + message: 'Protocol version must be in format "major.minor" (e.g., "1.0")', + value: message.protocolVersion, + }); + } + + if (errors.length > 0) { + return { valid: false, errors }; + } + + return { valid: true }; + } + + /** + * Perform full validation (schema + semantics) + */ + validateFull(message: unknown): ValidationResult { + // First, validate against JSON schema + const schemaResult = this.validate(message); + if (!schemaResult.valid) { + return schemaResult; + } + + // Then validate semantic rules + return this.validateSemantics(message as Message); + } + + /** + * Get validation error summary + */ + getErrorSummary(result: ValidationResult): string { + if (result.valid) { + return 'Valid'; + } + + return ( + result.errors?.map((err) => `${err.field}: ${err.message}`).join('; ') || + 'Unknown validation error' + ); + } +} + +/** + * Singleton validator instance + */ +export const validator = new MessageValidator(); diff --git a/packages/mcp-server/src/protocol/VersionManager.ts b/packages/mcp-server/src/protocol/VersionManager.ts new file mode 100644 index 0000000..7063a56 --- /dev/null +++ b/packages/mcp-server/src/protocol/VersionManager.ts @@ -0,0 +1,304 @@ +/** + * VersionManager - Protocol versioning and backward compatibility + * + * Supports backward compatibility for 1 major version: + * - Version 1.x can communicate with version 1.y + * - Version 2.x can communicate with version 1.y (with feature degradation) + * - Version 3.x cannot communicate with version 1.y + */ + +import { Message } from './Message.js'; + +/** + * Protocol version structure + */ +export interface ProtocolVersion { + major: number; + minor: number; +} + +/** + * Version compatibility result + */ +export interface CompatibilityResult { + compatible: boolean; + requiresDowngrade: boolean; + targetVersion?: string; + reason?: string; +} + +/** + * Version feature support + */ +export interface VersionFeatures { + version: string; + supportedMessageTypes: string[]; + requiredFields: string[]; + optionalFields: string[]; + deprecatedFields: string[]; +} + +/** + * VersionManager class for protocol versioning + */ +export class VersionManager { + /** + * Current protocol version + */ + public static readonly CURRENT_VERSION = '1.0'; + + /** + * Minimum supported version (backward compatibility limit) + */ + public static readonly MIN_SUPPORTED_VERSION = '1.0'; + + /** + * Maximum major version difference for compatibility + */ + public static readonly MAX_MAJOR_VERSION_DIFF = 1; + + /** + * Version feature matrix + */ + private static readonly VERSION_FEATURES: Map = new Map([ + [ + '1.0', + { + version: '1.0', + supportedMessageTypes: [ + 'task_assigned', + 'task_started', + 'task_blocked', + 'task_progress', + 'task_completed', + 'task_failed', + 'capability_query', + 'capability_response', + 'status_query', + 'status_response', + 'error', + 'heartbeat', + 'agent_joined', + 'agent_left', + ], + requiredFields: ['protocolVersion', 'messageType', 'senderId', 'timestamp'], + optionalFields: [ + 'recipientId', + 'taskId', + 'priority', + 'correlationId', + 'payload', + 'deliveryStatus', + ], + deprecatedFields: [], + }, + ], + ]); + + /** + * Parse version string to components + */ + static parseVersion(version: string): ProtocolVersion { + const match = version.match(/^(\d+)\.(\d+)$/); + if (!match) { + throw new Error(`Invalid version format: ${version}`); + } + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + }; + } + + /** + * Format version components to string + */ + static formatVersion(version: ProtocolVersion): string { + return `${version.major}.${version.minor}`; + } + + /** + * Compare two versions + * @returns -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 + */ + static compareVersions(v1: string, v2: string): number { + const ver1 = this.parseVersion(v1); + const ver2 = this.parseVersion(v2); + + if (ver1.major !== ver2.major) { + return ver1.major < ver2.major ? -1 : 1; + } + + if (ver1.minor !== ver2.minor) { + return ver1.minor < ver2.minor ? -1 : 1; + } + + return 0; + } + + /** + * Check if two versions are compatible + */ + static areVersionsCompatible( + localVersion: string, + remoteVersion: string + ): CompatibilityResult { + const local = this.parseVersion(localVersion); + const remote = this.parseVersion(remoteVersion); + + // Same version - fully compatible + if (local.major === remote.major && local.minor === remote.minor) { + return { compatible: true, requiresDowngrade: false }; + } + + // Same major version - compatible (minor version differences allowed) + if (local.major === remote.major) { + return { + compatible: true, + requiresDowngrade: local.minor > remote.minor, + targetVersion: remote.minor < local.minor ? remoteVersion : undefined, + }; + } + + // Different major versions - check if within compatibility window + const majorDiff = Math.abs(local.major - remote.major); + if (majorDiff <= this.MAX_MAJOR_VERSION_DIFF) { + // Backward compatibility: newer version can downgrade to older + if (local.major > remote.major) { + return { + compatible: true, + requiresDowngrade: true, + targetVersion: remoteVersion, + reason: `Downgrading from ${localVersion} to ${remoteVersion} for compatibility`, + }; + } + + // Forward compatibility: older version can receive newer messages + // (with potential field degradation) + return { + compatible: true, + requiresDowngrade: false, + reason: `Receiving messages from newer version ${remoteVersion}, some features may be unavailable`, + }; + } + + // Too many major versions apart - incompatible + return { + compatible: false, + requiresDowngrade: false, + reason: `Version ${localVersion} is incompatible with ${remoteVersion} (major version difference: ${majorDiff})`, + }; + } + + /** + * Get features supported by a version + */ + static getVersionFeatures(version: string): VersionFeatures | undefined { + return this.VERSION_FEATURES.get(version); + } + + /** + * Check if a message type is supported in a version + */ + static isMessageTypeSupported(version: string, messageType: string): boolean { + const features = this.getVersionFeatures(version); + return features?.supportedMessageTypes.includes(messageType) ?? false; + } + + /** + * Downgrade message to target version + * Removes fields not supported in target version + */ + static downgradeMessage(message: Message, targetVersion: string): Message { + const targetFeatures = this.getVersionFeatures(targetVersion); + if (!targetFeatures) { + throw new Error(`Unknown target version: ${targetVersion}`); + } + + // Create a copy of the message + const downgraded = { ...message }; + + // Update protocol version + downgraded.protocolVersion = targetVersion; + + // Remove deprecated fields + for (const field of targetFeatures.deprecatedFields) { + delete (downgraded as Record)[field]; + } + + // Check if message type is supported + if (!targetFeatures.supportedMessageTypes.includes(message.messageType)) { + throw new Error( + `Message type ${message.messageType} is not supported in version ${targetVersion}` + ); + } + + return downgraded; + } + + /** + * Upgrade message from older version + * Adds default values for new fields + */ + static upgradeMessage(message: Message, targetVersion: string): Message { + const targetFeatures = this.getVersionFeatures(targetVersion); + if (!targetFeatures) { + throw new Error(`Unknown target version: ${targetVersion}`); + } + + // Create a copy of the message + const upgraded = { ...message }; + + // Update protocol version + upgraded.protocolVersion = targetVersion; + + // Add default values for new optional fields if missing + // (In v1.0, all fields are already defined, so this is a placeholder for future versions) + + return upgraded; + } + + /** + * Negotiate protocol version between two agents + * Returns the highest mutually compatible version + */ + static negotiateVersion(localVersion: string, remoteVersion: string): string | null { + const compatibility = this.areVersionsCompatible(localVersion, remoteVersion); + + if (!compatibility.compatible) { + return null; + } + + // Use the lower version for communication + return this.compareVersions(localVersion, remoteVersion) <= 0 + ? localVersion + : remoteVersion; + } + + /** + * Validate version string format + */ + static isValidVersion(version: string): boolean { + try { + this.parseVersion(version); + return true; + } catch { + return false; + } + } + + /** + * Get version info + */ + static getVersionInfo(): { + current: string; + minSupported: string; + maxMajorDiff: number; + } { + return { + current: this.CURRENT_VERSION, + minSupported: this.MIN_SUPPORTED_VERSION, + maxMajorDiff: this.MAX_MAJOR_VERSION_DIFF, + }; + } +} diff --git a/packages/mcp-server/src/tasks/ConflictResolver.ts b/packages/mcp-server/src/tasks/ConflictResolver.ts new file mode 100644 index 0000000..9b97947 --- /dev/null +++ b/packages/mcp-server/src/tasks/ConflictResolver.ts @@ -0,0 +1,266 @@ +/** + * ConflictResolver - Timestamp-based first-come-first-served conflict resolution + * Resolves conflicts when multiple agents claim the same task simultaneously + */ + +import type { Task } from './Task.js'; +import type { Agent } from '../agents/Agent.js'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Task claim + */ +export interface TaskClaim { + /** Task being claimed */ + taskId: string; + + /** Agent claiming the task */ + agentId: string; + + /** Timestamp when claim was made */ + claimedAt: Date; + + /** Correlation ID for idempotency */ + correlationId?: string; +} + +/** + * Conflict resolution result + */ +export interface ConflictResolution { + /** Winner of the conflict */ + winner: TaskClaim; + + /** Losers of the conflict */ + losers: TaskClaim[]; + + /** Reason for resolution */ + reason: string; +} + +/** + * Conflict resolver configuration + */ +export interface ConflictResolverConfig { + /** Logger */ + logger?: Logger; + + /** Time window for detecting simultaneous claims (ms) */ + simultaneousWindowMs?: number; +} + +/** + * ConflictResolver class + */ +export class ConflictResolver { + private logger: Logger; + private simultaneousWindowMs: number; + private pendingClaims: Map; // taskId β†’ claims + private seenCorrelationIds: Set; // For idempotency + + constructor(config: ConflictResolverConfig = {}) { + this.logger = config.logger || createLogger(); + this.simultaneousWindowMs = config.simultaneousWindowMs || 1000; // 1 second + this.pendingClaims = new Map(); + this.seenCorrelationIds = new Set(); + } + + /** + * Register a task claim + * Returns winner immediately if no conflict, or after resolution window + */ + async registerClaim(claim: TaskClaim): Promise { + // Check for duplicate claim (idempotency) + if (claim.correlationId && this.seenCorrelationIds.has(claim.correlationId)) { + this.logger.debug('Duplicate claim ignored (idempotency)', { + taskId: claim.taskId, + agentId: claim.agentId, + correlationId: claim.correlationId, + }); + return null; + } + + // Record correlation ID + if (claim.correlationId) { + this.seenCorrelationIds.add(claim.correlationId); + } + + // Get or create claims list for this task + let claims = this.pendingClaims.get(claim.taskId); + if (!claims) { + claims = []; + this.pendingClaims.set(claim.taskId, claims); + } + + // Add claim to list + claims.push(claim); + + this.logger.debug('Task claim registered', { + taskId: claim.taskId, + agentId: claim.agentId, + claimedAt: claim.claimedAt, + totalClaims: claims.length, + }); + + // Wait for simultaneous window to detect conflicts + await this.waitForConflicts(claim.taskId); + + // Resolve conflicts + return this.resolveClaims(claim.taskId); + } + + /** + * Wait for potential simultaneous claims + */ + private async waitForConflicts(taskId: string): Promise { + return new Promise((resolve) => { + setTimeout(resolve, this.simultaneousWindowMs); + }); + } + + /** + * Resolve claims for a task + */ + private resolveClaims(taskId: string): ConflictResolution | null { + const claims = this.pendingClaims.get(taskId); + if (!claims || claims.length === 0) { + return null; + } + + // Remove from pending + this.pendingClaims.delete(taskId); + + // Single claim - no conflict + if (claims.length === 1) { + this.logger.debug('No conflict - single claim', { + taskId, + agentId: claims[0].agentId, + }); + return { + winner: claims[0], + losers: [], + reason: 'No conflict - single claimant', + }; + } + + // Multiple claims - resolve by earliest timestamp + const sorted = claims.sort( + (a, b) => a.claimedAt.getTime() - b.claimedAt.getTime() + ); + + const winner = sorted[0]; + const losers = sorted.slice(1); + + this.logger.info('Conflict resolved', { + taskId, + totalClaims: claims.length, + winner: winner.agentId, + losers: losers.map((c) => c.agentId), + timeDifference: sorted[sorted.length - 1].claimedAt.getTime() - winner.claimedAt.getTime(), + }); + + return { + winner, + losers, + reason: `First-come-first-served: ${winner.agentId} claimed at ${winner.claimedAt.toISOString()}`, + }; + } + + /** + * Check if a claim would create a conflict + */ + wouldConflict(taskId: string): boolean { + const claims = this.pendingClaims.get(taskId); + return claims !== undefined && claims.length > 0; + } + + /** + * Get pending claims for a task + */ + getPendingClaims(taskId: string): TaskClaim[] { + return this.pendingClaims.get(taskId) || []; + } + + /** + * Cancel a claim (e.g., if agent disconnects before resolution) + */ + cancelClaim(taskId: string, agentId: string): boolean { + const claims = this.pendingClaims.get(taskId); + if (!claims) { + return false; + } + + const index = claims.findIndex((c) => c.agentId === agentId); + if (index !== -1) { + claims.splice(index, 1); + this.logger.info('Claim cancelled', { taskId, agentId }); + + // Remove task from pending if no more claims + if (claims.length === 0) { + this.pendingClaims.delete(taskId); + } + + return true; + } + + return false; + } + + /** + * Clear all pending claims + */ + clear(): void { + this.pendingClaims.clear(); + this.seenCorrelationIds.clear(); + this.logger.info('All pending claims cleared'); + } + + /** + * Clean up old correlation IDs (to prevent memory leak) + */ + cleanupCorrelationIds(): void { + // In a production system, you'd want to expire these after some time + // For now, we'll keep a simple size limit + if (this.seenCorrelationIds.size > 10000) { + this.seenCorrelationIds.clear(); + this.logger.info('Correlation ID cache cleared'); + } + } + + /** + * Get statistics + */ + getStats(): { + pendingTasks: number; + totalPendingClaims: number; + cachedCorrelationIds: number; + } { + const totalClaims = Array.from(this.pendingClaims.values()).reduce( + (sum, claims) => sum + claims.length, + 0 + ); + + return { + pendingTasks: this.pendingClaims.size, + totalPendingClaims: totalClaims, + cachedCorrelationIds: this.seenCorrelationIds.size, + }; + } +} + +/** + * Create a task claim + */ +export function createTaskClaim( + taskId: string, + agentId: string, + correlationId?: string +): TaskClaim { + return { + taskId, + agentId, + claimedAt: new Date(), + correlationId, + }; +} diff --git a/packages/mcp-server/src/tasks/DependencyTracker.ts b/packages/mcp-server/src/tasks/DependencyTracker.ts new file mode 100644 index 0000000..c16a0cd --- /dev/null +++ b/packages/mcp-server/src/tasks/DependencyTracker.ts @@ -0,0 +1,390 @@ +/** + * DependencyTracker - Track task dependencies and notify when dependencies complete + * Manages task dependency graph and triggers notifications for unblocked tasks + */ + +import type { Task, TaskStatus } from './Task.js'; +import { TaskStatus as TaskStatusEnum, isTaskTerminal } from './Task.js'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Dependency event + */ +export interface DependencyEvent { + /** Task that was unblocked */ + task: Task; + + /** Dependencies that were completed */ + completedDependencies: string[]; + + /** Timestamp */ + timestamp: Date; +} + +/** + * Dependency event handler + */ +export type DependencyEventHandler = (event: DependencyEvent) => void | Promise; + +/** + * Dependency tracker configuration + */ +export interface DependencyTrackerConfig { + /** Logger */ + logger?: Logger; +} + +/** + * Dependency graph node + */ +interface DependencyNode { + taskId: string; + dependencies: Set; // Task IDs this task depends on + dependents: Set; // Task IDs that depend on this task + status: TaskStatus; +} + +/** + * DependencyTracker class + */ +export class DependencyTracker { + private logger: Logger; + private nodes: Map; // taskId β†’ node + private eventHandlers: Set; + + constructor(config: DependencyTrackerConfig = {}) { + this.logger = config.logger || createLogger(); + this.nodes = new Map(); + this.eventHandlers = new Set(); + } + + /** + * Add task to dependency graph + */ + addTask(task: Task): void { + // Create or update node + let node = this.nodes.get(task.id); + if (!node) { + node = { + taskId: task.id, + dependencies: new Set(task.dependencies), + dependents: new Set(), + status: task.status, + }; + this.nodes.set(task.id, node); + } else { + node.dependencies = new Set(task.dependencies); + node.status = task.status; + } + + // Update reverse dependencies (dependents) + for (const depId of task.dependencies) { + let depNode = this.nodes.get(depId); + if (!depNode) { + // Create placeholder node for dependency + depNode = { + taskId: depId, + dependencies: new Set(), + dependents: new Set(), + status: TaskStatusEnum.AVAILABLE, + }; + this.nodes.set(depId, depNode); + } + depNode.dependents.add(task.id); + } + + this.logger.debug('Task added to dependency tracker', { + taskId: task.id, + dependencies: task.dependencies, + status: task.status, + }); + } + + /** + * Update task status + */ + async updateTaskStatus(taskId: string, status: TaskStatus): Promise { + const node = this.nodes.get(taskId); + if (!node) { + this.logger.warn('Task not found in dependency tracker', { taskId }); + return; + } + + const previousStatus = node.status; + node.status = status; + + this.logger.debug('Task status updated', { + taskId, + previousStatus, + newStatus: status, + }); + + // If task is now completed, check if any dependents are unblocked + if (status === TaskStatusEnum.COMPLETED) { + await this.checkUnblockedDependents(taskId); + } + } + + /** + * Check if completing a task unblocks any dependents + */ + private async checkUnblockedDependents(completedTaskId: string): Promise { + const node = this.nodes.get(completedTaskId); + if (!node) { + return; + } + + // Check each dependent + for (const dependentId of node.dependents) { + const dependentNode = this.nodes.get(dependentId); + if (!dependentNode) { + continue; + } + + // Check if all dependencies are completed + const allDepsCompleted = Array.from(dependentNode.dependencies).every( + (depId) => { + const depNode = this.nodes.get(depId); + return depNode?.status === TaskStatusEnum.COMPLETED; + } + ); + + if (allDepsCompleted && dependentNode.status === TaskStatusEnum.AVAILABLE) { + // Task is unblocked! + const completedDeps = Array.from(dependentNode.dependencies); + + // Reconstruct task for event (simplified) + const task: Partial = { + id: dependentNode.taskId, + status: dependentNode.status, + dependencies: completedDeps, + }; + + await this.notifyHandlers({ + task: task as Task, + completedDependencies: completedDeps, + timestamp: new Date(), + }); + + this.logger.info('Task unblocked', { + taskId: dependentId, + completedDependencies: completedDeps, + }); + } + } + } + + /** + * Check if a task has all dependencies completed + */ + areDependenciesCompleted(taskId: string): boolean { + const node = this.nodes.get(taskId); + if (!node) { + return false; + } + + return Array.from(node.dependencies).every((depId) => { + const depNode = this.nodes.get(depId); + return depNode?.status === TaskStatusEnum.COMPLETED; + }); + } + + /** + * Get blocking dependencies for a task + */ + getBlockingDependencies(taskId: string): string[] { + const node = this.nodes.get(taskId); + if (!node) { + return []; + } + + return Array.from(node.dependencies).filter((depId) => { + const depNode = this.nodes.get(depId); + return depNode?.status !== TaskStatusEnum.COMPLETED; + }); + } + + /** + * Get all dependents of a task + */ + getDependents(taskId: string): string[] { + const node = this.nodes.get(taskId); + if (!node) { + return []; + } + + return Array.from(node.dependents); + } + + /** + * Get task dependency chain (recursive) + */ + getDependencyChain(taskId: string, visited: Set = new Set()): string[] { + if (visited.has(taskId)) { + // Circular dependency detected + this.logger.warn('Circular dependency detected', { taskId }); + return []; + } + + visited.add(taskId); + + const node = this.nodes.get(taskId); + if (!node) { + return []; + } + + const chain: string[] = []; + for (const depId of node.dependencies) { + chain.push(depId); + chain.push(...this.getDependencyChain(depId, visited)); + } + + return chain; + } + + /** + * Detect circular dependencies + */ + detectCircularDependencies(): string[][] { + const cycles: string[][] = []; + + for (const [taskId] of this.nodes) { + const visited = new Set(); + const path: string[] = []; + + const hasCycle = this.detectCycleFromNode(taskId, visited, path); + if (hasCycle && path.length > 0) { + cycles.push([...path]); + } + } + + return cycles; + } + + /** + * Detect cycle from a specific node (DFS) + */ + private detectCycleFromNode( + taskId: string, + visited: Set, + path: string[] + ): boolean { + if (path.includes(taskId)) { + // Found a cycle + path.push(taskId); + return true; + } + + if (visited.has(taskId)) { + return false; + } + + visited.add(taskId); + path.push(taskId); + + const node = this.nodes.get(taskId); + if (node) { + for (const depId of node.dependencies) { + if (this.detectCycleFromNode(depId, visited, path)) { + return true; + } + } + } + + path.pop(); + return false; + } + + /** + * Remove task from tracker + */ + removeTask(taskId: string): void { + const node = this.nodes.get(taskId); + if (!node) { + return; + } + + // Remove from dependents' dependency lists + for (const depId of node.dependencies) { + const depNode = this.nodes.get(depId); + if (depNode) { + depNode.dependents.delete(taskId); + } + } + + // Remove from dependencies' dependent lists + for (const dependentId of node.dependents) { + const dependentNode = this.nodes.get(dependentId); + if (dependentNode) { + dependentNode.dependencies.delete(taskId); + } + } + + this.nodes.delete(taskId); + this.logger.debug('Task removed from dependency tracker', { taskId }); + } + + /** + * Clear all tasks + */ + clear(): void { + this.nodes.clear(); + this.logger.info('Dependency tracker cleared'); + } + + /** + * Register dependency event handler + */ + onDependencyResolved(handler: DependencyEventHandler): () => void { + this.eventHandlers.add(handler); + return () => this.eventHandlers.delete(handler); + } + + /** + * Notify handlers + */ + private async notifyHandlers(event: DependencyEvent): Promise { + const promises = Array.from(this.eventHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in dependency handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Get statistics + */ + getStats(): { + totalTasks: number; + tasksWithDependencies: number; + averageDependencies: number; + circularDependencies: number; + } { + const tasksWithDeps = Array.from(this.nodes.values()).filter( + (node) => node.dependencies.size > 0 + ).length; + + const totalDeps = Array.from(this.nodes.values()).reduce( + (sum, node) => sum + node.dependencies.size, + 0 + ); + + const avgDeps = this.nodes.size > 0 ? totalDeps / this.nodes.size : 0; + + const cycles = this.detectCircularDependencies(); + + return { + totalTasks: this.nodes.size, + tasksWithDependencies: tasksWithDeps, + averageDependencies: Math.round(avgDeps * 100) / 100, + circularDependencies: cycles.length, + }; + } +} diff --git a/packages/mcp-server/src/tasks/Task.ts b/packages/mcp-server/src/tasks/Task.ts new file mode 100644 index 0000000..55fc13c --- /dev/null +++ b/packages/mcp-server/src/tasks/Task.ts @@ -0,0 +1,369 @@ +/** + * Task - Represents a work item from GitHub repository + * Based on specs/001-multi-agent-coordination/data-model.md + */ + +/** + * Task status states + */ +export enum TaskStatus { + AVAILABLE = 'available', + ASSIGNED = 'assigned', + STARTED = 'started', + IN_PROGRESS = 'in_progress', + BLOCKED = 'blocked', + COMPLETED = 'completed', + FAILED = 'failed', +} + +/** + * Task entity + */ +export interface Task { + /** Unique task identifier (UUID v4) */ + id: string; + + /** Task description */ + description: string; + + /** Array of assigned agent IDs */ + assignedAgents: string[]; + + /** Task state */ + status: TaskStatus; + + /** Array of task IDs this task depends on */ + dependencies: string[]; + + /** GitHub issue number */ + githubIssueId: string; + + /** Full GitHub issue URL */ + githubIssueUrl: string; + + /** Associated PR URL (optional) */ + githubPRUrl?: string | null; + + /** When task was created */ + createdAt: Date; + + /** When task was assigned (optional) */ + assignedAt?: Date | null; + + /** When work started (optional) */ + startedAt?: Date | null; + + /** When work finished (optional) */ + completedAt?: Date | null; + + /** Timestamp for conflict resolution */ + claimedAt?: Date | null; + + /** Progress percentage (0-100) */ + percentComplete?: number; + + /** Current status message */ + statusMessage?: string; +} + +/** + * Task creation data + */ +export interface TaskCreation { + /** Task description */ + description: string; + + /** Array of task IDs this task depends on */ + dependencies?: string[]; + + /** GitHub issue number */ + githubIssueId: string; + + /** Full GitHub issue URL */ + githubIssueUrl: string; +} + +/** + * Task update data + */ +export interface TaskUpdate { + /** Update task status */ + status?: TaskStatus; + + /** Update assigned agents */ + assignedAgents?: string[]; + + /** Update GitHub PR URL */ + githubPRUrl?: string | null; + + /** Update assigned timestamp */ + assignedAt?: Date | null; + + /** Update started timestamp */ + startedAt?: Date | null; + + /** Update completed timestamp */ + completedAt?: Date | null; + + /** Update progress percentage */ + percentComplete?: number; + + /** Update status message */ + statusMessage?: string; +} + +/** + * Task query filter + */ +export interface TaskQuery { + /** Filter by task status */ + status?: TaskStatus; + + /** Filter by assigned agent ID */ + assignedToAgent?: string; + + /** Filter by whether task has dependencies */ + hasDependencies?: boolean; + + /** Filter by whether dependencies are complete */ + dependenciesComplete?: boolean; + + /** Filter by GitHub issue ID */ + githubIssueId?: string; + + /** Filter by creation date range */ + createdAfter?: Date; + createdBefore?: Date; +} + +/** + * Validate task object + */ +export function validateTask(task: unknown): task is Task { + if (typeof task !== 'object' || task === null) { + return false; + } + + const t = task as Partial; + + return ( + typeof t.id === 'string' && + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + t.id + ) && + typeof t.description === 'string' && + t.description.length > 0 && + t.description.length <= 500 && + Array.isArray(t.assignedAgents) && + t.assignedAgents.every((id) => typeof id === 'string') && + typeof t.status === 'string' && + Object.values(TaskStatus).includes(t.status as TaskStatus) && + Array.isArray(t.dependencies) && + t.dependencies.every((id) => typeof id === 'string') && + typeof t.githubIssueId === 'string' && + typeof t.githubIssueUrl === 'string' && + t.createdAt instanceof Date + ); +} + +/** + * Check if a task matches a query + */ +export function matchesTaskQuery( + task: Task, + query: TaskQuery, + allTasks?: Map +): boolean { + if (query.status && task.status !== query.status) { + return false; + } + + if (query.assignedToAgent && !task.assignedAgents.includes(query.assignedToAgent)) { + return false; + } + + if (query.hasDependencies !== undefined) { + const hasDeps = task.dependencies.length > 0; + if (query.hasDependencies !== hasDeps) { + return false; + } + } + + if (query.dependenciesComplete !== undefined && allTasks) { + const depsComplete = task.dependencies.every((depId) => { + const dep = allTasks.get(depId); + return dep?.status === TaskStatus.COMPLETED; + }); + if (query.dependenciesComplete !== depsComplete) { + return false; + } + } + + if (query.githubIssueId && task.githubIssueId !== query.githubIssueId) { + return false; + } + + if (query.createdAfter && task.createdAt < query.createdAfter) { + return false; + } + + if (query.createdBefore && task.createdAt > query.createdBefore) { + return false; + } + + return true; +} + +/** + * Create a task from creation data + */ +export function createTask(id: string, data: TaskCreation): Task { + return { + id, + description: data.description, + assignedAgents: [], + status: TaskStatus.AVAILABLE, + dependencies: data.dependencies || [], + githubIssueId: data.githubIssueId, + githubIssueUrl: data.githubIssueUrl, + githubPRUrl: null, + createdAt: new Date(), + assignedAt: null, + startedAt: null, + completedAt: null, + claimedAt: null, + percentComplete: 0, + }; +} + +/** + * Update a task with partial data + */ +export function updateTask(task: Task, update: TaskUpdate): Task { + return { + ...task, + ...(update.status !== undefined && { status: update.status }), + ...(update.assignedAgents !== undefined && { + assignedAgents: update.assignedAgents, + }), + ...(update.githubPRUrl !== undefined && { githubPRUrl: update.githubPRUrl }), + ...(update.assignedAt !== undefined && { assignedAt: update.assignedAt }), + ...(update.startedAt !== undefined && { startedAt: update.startedAt }), + ...(update.completedAt !== undefined && { completedAt: update.completedAt }), + ...(update.percentComplete !== undefined && { + percentComplete: update.percentComplete, + }), + ...(update.statusMessage !== undefined && { + statusMessage: update.statusMessage, + }), + }; +} + +/** + * Assign a task to an agent + */ +export function assignTask(task: Task, agentId: string): Task { + return updateTask(task, { + status: TaskStatus.ASSIGNED, + assignedAgents: [...task.assignedAgents, agentId], + assignedAt: new Date(), + }); +} + +/** + * Start a task + */ +export function startTask(task: Task): Task { + return updateTask(task, { + status: TaskStatus.STARTED, + startedAt: new Date(), + percentComplete: 0, + }); +} + +/** + * Mark task as in progress + */ +export function progressTask( + task: Task, + percentComplete: number, + statusMessage?: string +): Task { + return updateTask(task, { + status: TaskStatus.IN_PROGRESS, + percentComplete: Math.max(0, Math.min(100, percentComplete)), + statusMessage, + }); +} + +/** + * Block a task + */ +export function blockTask(task: Task, reason: string): Task { + return updateTask(task, { + status: TaskStatus.BLOCKED, + statusMessage: reason, + }); +} + +/** + * Complete a task + */ +export function completeTask(task: Task, githubPRUrl?: string): Task { + return updateTask(task, { + status: TaskStatus.COMPLETED, + completedAt: new Date(), + percentComplete: 100, + githubPRUrl: githubPRUrl || task.githubPRUrl, + }); +} + +/** + * Fail a task + */ +export function failTask(task: Task, error: string): Task { + return updateTask(task, { + status: TaskStatus.FAILED, + completedAt: new Date(), + statusMessage: error, + }); +} + +/** + * Check if task is available for assignment + */ +export function isTaskAvailable(task: Task, allTasks?: Map): boolean { + if (task.status !== TaskStatus.AVAILABLE) { + return false; + } + + // Check if all dependencies are completed + if (allTasks) { + return task.dependencies.every((depId) => { + const dep = allTasks.get(depId); + return dep?.status === TaskStatus.COMPLETED; + }); + } + + // If no task map provided, just check status + return task.dependencies.length === 0; +} + +/** + * Check if task is in a terminal state + */ +export function isTaskTerminal(task: Task): boolean { + return [TaskStatus.COMPLETED, TaskStatus.FAILED].includes(task.status); +} + +/** + * Get task duration in milliseconds + */ +export function getTaskDuration(task: Task): number | null { + if (!task.startedAt) { + return null; + } + + const endTime = task.completedAt || new Date(); + return endTime.getTime() - task.startedAt.getTime(); +} diff --git a/packages/mcp-server/src/tasks/TaskQueue.ts b/packages/mcp-server/src/tasks/TaskQueue.ts new file mode 100644 index 0000000..dcbfb3c --- /dev/null +++ b/packages/mcp-server/src/tasks/TaskQueue.ts @@ -0,0 +1,483 @@ +/** + * TaskQueue - FIFO queue with task assignment logic + * Manages available tasks and assigns them to agents based on capabilities + */ + +import type { Task, TaskStatus } from './Task.js'; +import { isTaskAvailable, assignTask, TaskStatus as TaskStatusEnum } from './Task.js'; +import type { Agent } from '../agents/Agent.js'; +import { isAgentAvailable } from '../agents/Agent.js'; +import type { Logger } from '../logging/Logger.js'; +import { createLogger } from '../logging/Logger.js'; + +/** + * Task assignment event + */ +export interface TaskAssignmentEvent { + task: Task; + agent: Agent; + timestamp: Date; +} + +/** + * Task assignment handler + */ +export type TaskAssignmentHandler = (event: TaskAssignmentEvent) => void | Promise; + +/** + * Task lifecycle event types + */ +export type TaskLifecycleEventType = + | 'task_assigned' + | 'task_started' + | 'task_blocked' + | 'task_progress' + | 'task_completed' + | 'task_failed'; + +/** + * Task lifecycle event + */ +export interface TaskLifecycleEvent { + type: TaskLifecycleEventType; + task: Task; + agent?: Agent; + metadata?: Record; + timestamp: Date; +} + +/** + * Task lifecycle handler + */ +export type TaskLifecycleHandler = (event: TaskLifecycleEvent) => void | Promise; + +/** + * Task queue configuration + */ +export interface TaskQueueConfig { + /** Logger */ + logger?: Logger; + + /** Maximum queue size */ + maxQueueSize?: number; +} + +/** + * TaskQueue class + */ +export class TaskQueue { + private queue: Task[]; + private assignedTasks: Map; // taskId β†’ Task + private logger: Logger; + private maxQueueSize: number; + private assignmentHandlers: Set; + private lifecycleHandlers: Set; + + constructor(config: TaskQueueConfig = {}) { + this.queue = []; + this.assignedTasks = new Map(); + this.logger = config.logger || createLogger(); + this.maxQueueSize = config.maxQueueSize || 1000; + this.assignmentHandlers = new Set(); + this.lifecycleHandlers = new Set(); + } + + /** + * Add task to queue + */ + enqueue(task: Task, allTasks?: Map): void { + // Check if task is already in queue or assigned + if (this.isTaskInQueue(task.id) || this.assignedTasks.has(task.id)) { + this.logger.warn('Task already in queue or assigned', { taskId: task.id }); + return; + } + + // Check queue size limit + if (this.queue.length >= this.maxQueueSize) { + throw new Error(`Queue is full (max: ${this.maxQueueSize})`); + } + + // Check if task is available + if (!isTaskAvailable(task, allTasks)) { + this.logger.warn('Task is not available for assignment', { + taskId: task.id, + status: task.status, + }); + return; + } + + this.queue.push(task); + this.logger.info('Task added to queue', { + taskId: task.id, + queueSize: this.queue.length, + }); + } + + /** + * Remove task from queue (FIFO) + */ + dequeue(): Task | undefined { + const task = this.queue.shift(); + if (task) { + this.logger.debug('Task dequeued', { + taskId: task.id, + queueSize: this.queue.length, + }); + } + return task; + } + + /** + * Peek at next task without removing + */ + peek(): Task | undefined { + return this.queue[0]; + } + + /** + * Assign next available task to an agent + */ + async assignNext(agent: Agent, allTasks?: Map): Promise { + // Check if agent is available + if (!isAgentAvailable(agent)) { + this.logger.warn('Agent is not available', { + agentId: agent.id, + status: agent.status, + currentTask: agent.currentTask, + }); + return null; + } + + // Find first task that matches agent capabilities + const taskIndex = this.queue.findIndex((task) => { + return ( + isTaskAvailable(task, allTasks) && + this.isTaskSuitableForAgent(task, agent) + ); + }); + + if (taskIndex === -1) { + this.logger.debug('No suitable tasks available for agent', { + agentId: agent.id, + queueSize: this.queue.length, + }); + return null; + } + + // Remove task from queue + const [task] = this.queue.splice(taskIndex, 1); + + // Assign task to agent + const assignedTask = assignTask(task, agent.id); + + // Store in assigned tasks + this.assignedTasks.set(assignedTask.id, assignedTask); + + // Notify handlers + await this.notifyAssignmentHandlers({ + task: assignedTask, + agent, + timestamp: new Date(), + }); + + this.logger.info('Task assigned to agent', { + taskId: assignedTask.id, + agentId: agent.id, + queueSize: this.queue.length, + }); + + return assignedTask; + } + + /** + * Check if task is suitable for agent (based on capabilities) + */ + private isTaskSuitableForAgent(task: Task, agent: Agent): boolean { + // Basic suitability check + // Can be extended with more sophisticated matching logic + + // For now, all tasks are suitable for all agents + // In the future, could match task requirements with agent capabilities + return true; + } + + /** + * Mark task as completed (remove from assigned) + */ + complete(taskId: string): void { + if (this.assignedTasks.has(taskId)) { + this.assignedTasks.delete(taskId); + this.logger.info('Task marked as completed', { taskId }); + } + } + + /** + * Return task to queue (unassign) + */ + unassign(taskId: string): void { + const task = this.assignedTasks.get(taskId); + if (task) { + this.assignedTasks.delete(taskId); + + // Reset task assignment + const unassignedTask: Task = { + ...task, + assignedAgents: [], + status: TaskStatusEnum.AVAILABLE, + assignedAt: null, + }; + + this.queue.unshift(unassignedTask); // Add to front of queue + this.logger.info('Task returned to queue', { + taskId, + queueSize: this.queue.length, + }); + } + } + + /** + * Check if task is in queue + */ + isTaskInQueue(taskId: string): boolean { + return this.queue.some((task) => task.id === taskId); + } + + /** + * Get task from queue by ID + */ + getTaskById(taskId: string): Task | undefined { + return ( + this.queue.find((task) => task.id === taskId) || + this.assignedTasks.get(taskId) + ); + } + + /** + * Get all tasks in queue + */ + getAllTasks(): Task[] { + return [...this.queue]; + } + + /** + * Get all assigned tasks + */ + getAssignedTasks(): Task[] { + return Array.from(this.assignedTasks.values()); + } + + /** + * Get queue size + */ + size(): number { + return this.queue.length; + } + + /** + * Get assigned tasks count + */ + assignedCount(): number { + return this.assignedTasks.size; + } + + /** + * Clear queue + */ + clear(): void { + this.queue = []; + this.assignedTasks.clear(); + this.logger.info('Queue cleared'); + } + + /** + * Remove specific task from queue + */ + remove(taskId: string): boolean { + const index = this.queue.findIndex((task) => task.id === taskId); + if (index !== -1) { + this.queue.splice(index, 1); + this.logger.info('Task removed from queue', { + taskId, + queueSize: this.queue.length, + }); + return true; + } + return false; + } + + /** + * Register assignment handler + */ + onAssignment(handler: TaskAssignmentHandler): () => void { + this.assignmentHandlers.add(handler); + return () => this.assignmentHandlers.delete(handler); + } + + /** + * Notify assignment handlers + */ + private async notifyAssignmentHandlers(event: TaskAssignmentEvent): Promise { + const promises = Array.from(this.assignmentHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in assignment handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Get queue statistics + */ + getStats(): { + queueSize: number; + assignedCount: number; + totalProcessed: number; + } { + return { + queueSize: this.queue.length, + assignedCount: this.assignedTasks.size, + totalProcessed: this.assignedTasks.size, // Simplified; could track separately + }; + } + + /** + * Register lifecycle event handler + */ + onLifecycle(handler: TaskLifecycleHandler): () => void { + this.lifecycleHandlers.add(handler); + return () => this.lifecycleHandlers.delete(handler); + } + + /** + * Emit task lifecycle event + */ + async emitLifecycleEvent( + type: TaskLifecycleEventType, + task: Task, + agent?: Agent, + metadata?: Record + ): Promise { + const event: TaskLifecycleEvent = { + type, + task, + agent, + metadata, + timestamp: new Date(), + }; + + await this.notifyLifecycleHandlers(event); + } + + /** + * Notify lifecycle handlers + */ + private async notifyLifecycleHandlers(event: TaskLifecycleEvent): Promise { + const promises = Array.from(this.lifecycleHandlers).map(async (handler) => { + try { + await handler(event); + } catch (error) { + this.logger.error('Error in lifecycle handler', { + error: error instanceof Error ? error : new Error(String(error)), + }); + } + }); + + await Promise.all(promises); + } + + /** + * Mark task as started + */ + async markStarted(taskId: string, agent: Agent): Promise { + const task = this.assignedTasks.get(taskId); + if (task) { + await this.emitLifecycleEvent('task_started', task, agent); + this.logger.info('Task started', { taskId, agentId: agent.id }); + } + } + + /** + * Mark task as blocked + */ + async markBlocked( + taskId: string, + agent: Agent, + reason: string, + blockedBy: string[] + ): Promise { + const task = this.assignedTasks.get(taskId); + if (task) { + await this.emitLifecycleEvent('task_blocked', task, agent, { reason, blockedBy }); + this.logger.info('Task blocked', { taskId, agentId: agent.id, reason }); + } + } + + /** + * Update task progress + */ + async updateProgress( + taskId: string, + agent: Agent, + percentComplete: number, + status: string + ): Promise { + const task = this.assignedTasks.get(taskId); + if (task) { + await this.emitLifecycleEvent('task_progress', task, agent, { + percentComplete, + status, + }); + this.logger.debug('Task progress updated', { + taskId, + agentId: agent.id, + percentComplete, + }); + } + } + + /** + * Mark task as completed + */ + async markCompleted( + taskId: string, + agent: Agent, + result?: Record + ): Promise { + const task = this.assignedTasks.get(taskId); + if (task) { + await this.emitLifecycleEvent('task_completed', task, agent, { result }); + this.complete(taskId); // Remove from assigned + this.logger.info('Task completed', { taskId, agentId: agent.id }); + } + } + + /** + * Mark task as failed + */ + async markFailed( + taskId: string, + agent: Agent, + error: string, + retryable: boolean + ): Promise { + const task = this.assignedTasks.get(taskId); + if (task) { + await this.emitLifecycleEvent('task_failed', task, agent, { error, retryable }); + + if (retryable) { + // Return task to queue for retry + this.unassign(taskId); + } else { + // Remove from assigned + this.complete(taskId); + } + + this.logger.warn('Task failed', { taskId, agentId: agent.id, error, retryable }); + } + } +} diff --git a/packages/mcp-server/tests/integration/agent-task-coordination.test.ts b/packages/mcp-server/tests/integration/agent-task-coordination.test.ts new file mode 100644 index 0000000..56aefdf --- /dev/null +++ b/packages/mcp-server/tests/integration/agent-task-coordination.test.ts @@ -0,0 +1,320 @@ +/** + * Integration Test: Agent Task Coordination + * Tests the complete workflow from GitHub issue β†’ task assignment β†’ agent coordination + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { v4 as uuidv4 } from 'uuid'; +import { Agent, AgentStatus, createAgent } from '../../src/agents/Agent.js'; +import { Capability, Platform } from '../../src/agents/Capability.js'; +import { AgentRegistry } from '../../src/agents/AgentRegistry.js'; +import { RoleManager } from '../../src/agents/RoleManager.js'; +import { Task, TaskStatus, createTask } from '../../src/tasks/Task.js'; +import { TaskQueue } from '../../src/tasks/TaskQueue.js'; +import { DependencyTracker } from '../../src/tasks/DependencyTracker.js'; +import { MessageBuilder } from '../../src/protocol/MessageBuilder.js'; +import { MessageType } from '../../src/protocol/Message.js'; + +describe('Agent Task Coordination', () => { + let agentRegistry: AgentRegistry; + let roleManager: RoleManager; + let taskQueue: TaskQueue; + let dependencyTracker: DependencyTracker; + let developerAgent: Agent; + let testerAgent: Agent; + + beforeEach(() => { + // Initialize components + agentRegistry = new AgentRegistry({ enableTimeoutChecking: false }); + roleManager = new RoleManager(); + taskQueue = new TaskQueue(); + dependencyTracker = new DependencyTracker(); + + // Create test agents + const developerCapability: Capability = { + agentId: uuidv4(), + roleType: 'developer', + platform: 'Linux' as Platform, + tools: ['git', 'npm', 'docker', 'typescript'], + languages: ['TypeScript', 'JavaScript'], + }; + + const testerCapability: Capability = { + agentId: uuidv4(), + roleType: 'tester', + platform: 'Linux' as Platform, + tools: ['jest', 'playwright', 'docker'], + languages: ['TypeScript', 'JavaScript'], + }; + + developerAgent = createAgent(developerCapability.agentId, { + role: 'developer', + platform: 'Linux' as Platform, + environment: 'GitHub Actions', + capabilities: developerCapability, + }); + developerAgent.status = AgentStatus.CONNECTED; + + testerAgent = createAgent(testerCapability.agentId, { + role: 'tester', + platform: 'Linux' as Platform, + environment: 'GitHub Actions', + capabilities: testerCapability, + }); + testerAgent.status = AgentStatus.CONNECTED; + }); + + afterEach(() => { + agentRegistry.clear(); + taskQueue.clear(); + dependencyTracker.clear(); + }); + + it('should register agents successfully', async () => { + await agentRegistry.add(developerAgent); + await agentRegistry.add(testerAgent); + + expect(agentRegistry.count()).toBe(2); + expect(agentRegistry.getById(developerAgent.id)).toBeDefined(); + expect(agentRegistry.getById(testerAgent.id)).toBeDefined(); + }); + + it('should assign task to available agent', async () => { + await agentRegistry.add(developerAgent); + + // Create a task + const task = createTask(uuidv4(), { + description: 'Implement user authentication', + githubIssueId: '42', + githubIssueUrl: 'https://github.com/org/repo/issues/42', + }); + + // Add to queue + taskQueue.enqueue(task); + + // Assign to developer agent + const assignedTask = await taskQueue.assignNext(developerAgent); + + expect(assignedTask).toBeDefined(); + expect(assignedTask?.id).toBe(task.id); + expect(assignedTask?.assignedAgents).toContain(developerAgent.id); + expect(assignedTask?.status).toBe(TaskStatus.ASSIGNED); + }); + + it('should emit task lifecycle events', async () => { + const lifecycleEvents: string[] = []; + + taskQueue.onLifecycle((event) => { + lifecycleEvents.push(event.type); + }); + + await agentRegistry.add(developerAgent); + + const task = createTask(uuidv4(), { + description: 'Fix authentication bug', + githubIssueId: '43', + githubIssueUrl: 'https://github.com/org/repo/issues/43', + }); + + taskQueue.enqueue(task); + const assignedTask = await taskQueue.assignNext(developerAgent); + + expect(assignedTask).toBeDefined(); + + // Simulate task lifecycle + await taskQueue.markStarted(assignedTask!.id, developerAgent); + await taskQueue.updateProgress(assignedTask!.id, developerAgent, 50, 'In progress'); + await taskQueue.markCompleted(assignedTask!.id, developerAgent, { success: true }); + + expect(lifecycleEvents).toContain('task_started'); + expect(lifecycleEvents).toContain('task_progress'); + expect(lifecycleEvents).toContain('task_completed'); + }); + + it('should handle task dependencies correctly', async () => { + // Create task chain: task2 depends on task1 + const task1 = createTask(uuidv4(), { + description: 'Task 1', + githubIssueId: '1', + githubIssueUrl: 'https://github.com/org/repo/issues/1', + }); + + const task2 = createTask(uuidv4(), { + description: 'Task 2 (depends on Task 1)', + dependencies: [task1.id], + githubIssueId: '2', + githubIssueUrl: 'https://github.com/org/repo/issues/2', + }); + + // Add to dependency tracker + dependencyTracker.addTask(task1); + dependencyTracker.addTask(task2); + + // Task 2 should be blocked initially + expect(dependencyTracker.areDependenciesCompleted(task2.id)).toBe(false); + expect(dependencyTracker.getBlockingDependencies(task2.id)).toContain(task1.id); + + // Complete task 1 + await dependencyTracker.updateTaskStatus(task1.id, TaskStatus.COMPLETED); + + // Task 2 should now be unblocked + expect(dependencyTracker.areDependenciesCompleted(task2.id)).toBe(true); + expect(dependencyTracker.getBlockingDependencies(task2.id)).toHaveLength(0); + }); + + it('should notify when dependencies are resolved', async () => { + let unblockedTaskId: string | undefined; + + dependencyTracker.onDependencyResolved((event) => { + unblockedTaskId = event.task.id; + }); + + const task1 = createTask(uuidv4(), { + description: 'Task 1', + githubIssueId: '1', + githubIssueUrl: 'https://github.com/org/repo/issues/1', + }); + + const task2 = createTask(uuidv4(), { + description: 'Task 2', + dependencies: [task1.id], + githubIssueId: '2', + githubIssueUrl: 'https://github.com/org/repo/issues/2', + }); + + dependencyTracker.addTask(task1); + dependencyTracker.addTask(task2); + + // Complete task 1 + await dependencyTracker.updateTaskStatus(task1.id, TaskStatus.COMPLETED); + + // Wait for async notification + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(unblockedTaskId).toBe(task2.id); + }); + + it('should build protocol messages correctly', () => { + const taskId = uuidv4(); + const agentId = developerAgent.id; + + // Test task_assigned message + const assignedMsg = MessageBuilder.taskAssigned( + agentId, + testerAgent.id, + { + taskId, + description: 'Test task', + githubIssue: 'https://github.com/org/repo/issues/1', + } + ); + + expect(assignedMsg.messageType).toBe(MessageType.TASK_ASSIGNED); + expect(assignedMsg.senderId).toBe(agentId); + expect(assignedMsg.recipientId).toBe(testerAgent.id); + expect(assignedMsg.taskId).toBe(taskId); + expect(assignedMsg.payload).toBeDefined(); + + // Test task_completed message + const completedMsg = MessageBuilder.taskCompleted(agentId, { + taskId, + result: { success: true }, + githubPR: 'https://github.com/org/repo/pull/1', + }); + + expect(completedMsg.messageType).toBe(MessageType.TASK_COMPLETED); + expect(completedMsg.senderId).toBe(agentId); + expect(completedMsg.taskId).toBe(taskId); + }); + + it('should manage agent roles correctly', () => { + // Predefined roles should exist + expect(roleManager.hasRole('developer')).toBe(true); + expect(roleManager.hasRole('tester')).toBe(true); + expect(roleManager.hasRole('architect')).toBe(true); + + // Should be able to register custom role + const customRole = roleManager.registerCustomRole( + 'ml-engineer', + 'Machine Learning Engineer', + { + tools: ['python', 'tensorflow', 'jupyter'], + languages: ['Python'], + } + ); + + expect(customRole.name).toBe('ml-engineer'); + expect(customRole.type).toBe('custom'); + expect(roleManager.hasRole('ml-engineer')).toBe(true); + }); + + it('should suggest roles based on capabilities', () => { + const suggestions = roleManager.suggestRoles({ + tools: ['git', 'npm', 'docker'], + languages: ['TypeScript', 'JavaScript'], + }); + + // Should suggest developer role (has matching tools/languages) + const roleNames = suggestions.map((r) => r.name); + expect(roleNames).toContain('developer'); + }); + + it('should handle complete workflow: GitHub issue β†’ task β†’ agent β†’ completion', async () => { + // Step 1: Register agents + await agentRegistry.add(developerAgent); + await agentRegistry.add(testerAgent); + + // Step 2: Create task from GitHub issue + const task = createTask(uuidv4(), { + description: 'Implement user authentication feature', + githubIssueId: '42', + githubIssueUrl: 'https://github.com/org/repo/issues/42', + }); + + // Step 3: Add task to queue + taskQueue.enqueue(task); + + // Step 4: Assign task to developer agent + const assignedTask = await taskQueue.assignNext(developerAgent); + expect(assignedTask).toBeDefined(); + + // Step 5: Developer starts work + await taskQueue.markStarted(assignedTask!.id, developerAgent); + + // Step 6: Developer reports progress + await taskQueue.updateProgress(assignedTask!.id, developerAgent, 50, 'Implementing auth logic'); + + // Step 7: Developer completes task + await taskQueue.markCompleted(assignedTask!.id, developerAgent, { + pullRequest: 'https://github.com/org/repo/pull/100', + }); + + // Verify task is no longer in assigned tasks + expect(taskQueue.getTaskById(assignedTask!.id)).toBeUndefined(); + }); + + it('should handle agent timeout detection', async () => { + const registry = new AgentRegistry({ + enableTimeoutChecking: true, + timeoutMs: 1000, // 1 second + }); + + let timedOutAgent: Agent | undefined; + + registry.onEvent((event) => { + if (event.type === 'agent_timeout') { + timedOutAgent = event.agent; + } + }); + + await registry.add(developerAgent); + + // Wait for timeout + await new Promise((resolve) => setTimeout(resolve, 1500)); + + expect(timedOutAgent).toBeDefined(); + expect(timedOutAgent?.id).toBe(developerAgent.id); + + registry.destroy(); + }); +}); diff --git a/packages/mcp-server/tests/integration/secure-communication.test.ts b/packages/mcp-server/tests/integration/secure-communication.test.ts new file mode 100644 index 0000000..e60d72a --- /dev/null +++ b/packages/mcp-server/tests/integration/secure-communication.test.ts @@ -0,0 +1,329 @@ +/** + * Integration Test: Secure Agent Communication + * Tests authentication, encryption, and security features + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { TokenGenerator, generateChannelToken, hashToken } from '../../src/config/TokenGenerator.js'; +import type { ChannelConfig } from '../../src/channels/base/Channel.js'; +import { ChannelAdapter } from '../../src/channels/base/ChannelAdapter.js'; +import { Message, MessageType } from '../../src/protocol/Message.js'; +import { MessageBuilder } from '../../src/protocol/MessageBuilder.js'; + +describe('Secure Communication', () => { + describe('TokenGenerator', () => { + it('should generate secure random tokens', () => { + const token1 = TokenGenerator.generate(); + const token2 = TokenGenerator.generate(); + + expect(token1).toBeDefined(); + expect(token2).toBeDefined(); + expect(token1).not.toBe(token2); // Should be unique + expect(token1.length).toBeGreaterThanOrEqual(32); // Minimum length + }); + + it('should generate channel tokens with correct format', () => { + const token = TokenGenerator.generateChannelToken(); + + expect(token).toMatch(/^cct_[a-f0-9]+$/); + expect(token.length).toBeGreaterThan(16); + }); + + it('should generate API tokens with correct format', () => { + const token = TokenGenerator.generateAPIToken(); + + expect(token).toMatch(/^cca_[a-zA-Z0-9_-]+$/); + expect(token.length).toBeGreaterThan(16); + }); + + it('should validate token format correctly', () => { + const validToken = 'cct_' + 'a'.repeat(32); + const shortToken = 'cct_abc'; + const invalidChars = 'cct_abc$@!'; + + expect(TokenGenerator.validateFormat(validToken, { prefix: 'cct_' })).toBe(true); + expect(TokenGenerator.validateFormat(shortToken, { minLength: 16 })).toBe(false); + expect(TokenGenerator.validateFormat(invalidChars)).toBe(false); + }); + + it('should hash tokens consistently', () => { + const token = 'test_token_123'; + const hash1 = TokenGenerator.hash(token); + const hash2 = TokenGenerator.hash(token); + + expect(hash1).toBe(hash2); + expect(hash1).toHaveLength(64); // SHA-256 produces 64 hex characters + expect(hash1).toMatch(/^[a-f0-9]+$/); + }); + + it('should produce different hashes for different tokens', () => { + const token1 = 'token_one'; + const token2 = 'token_two'; + const hash1 = TokenGenerator.hash(token1); + const hash2 = TokenGenerator.hash(token2); + + expect(hash1).not.toBe(hash2); + }); + }); + + describe('Channel Authentication', () => { + class TestChannel extends ChannelAdapter { + protected async doConnect(): Promise { + // Mock implementation + } + + protected async doDisconnect(): Promise { + // Mock implementation + } + + protected async doSendMessage(message: Message): Promise { + // Mock implementation + } + + protected async doPing(): Promise { + // Mock implementation + } + + // Expose protected methods for testing + public testVerifyToken(token: string): boolean { + return this.verifyToken(token); + } + + public testGetAuthToken(): string { + return this.getAuthToken(); + } + } + + it('should reject channels with invalid tokens', () => { + const invalidConfigs = [ + { token: '' }, // Empty token + { token: 'short' }, // Too short + { token: '12345' }, // Less than 16 chars + ]; + + for (const params of invalidConfigs) { + expect(() => { + new TestChannel({ + type: 'test', + token: params.token, + connectionParams: {}, + } as ChannelConfig); + }).toThrow('Invalid or missing authentication token'); + } + }); + + it('should accept channels with valid tokens', () => { + const validToken = generateChannelToken(); + + expect(() => { + new TestChannel({ + type: 'test', + token: validToken, + connectionParams: {}, + } as ChannelConfig); + }).not.toThrow(); + }); + + it('should verify tokens using timing-safe comparison', () => { + const validToken = generateChannelToken(); + const channel = new TestChannel({ + type: 'test', + token: validToken, + connectionParams: {}, + } as ChannelConfig); + + // Valid token should pass + expect(channel.testVerifyToken(validToken)).toBe(true); + + // Invalid tokens should fail + expect(channel.testVerifyToken('wrong_token')).toBe(false); + expect(channel.testVerifyToken('')).toBe(false); + expect(channel.testVerifyToken(validToken + 'extra')).toBe(false); + }); + + it('should provide auth token for connections', () => { + const token = generateChannelToken(); + const channel = new TestChannel({ + type: 'test', + token, + connectionParams: {}, + } as ChannelConfig); + + expect(channel.testGetAuthToken()).toBe(token); + }); + }); + + describe('Message Security', () => { + it('should include authentication metadata in messages', () => { + const senderId = 'agent-123'; + const message = MessageBuilder.heartbeat(senderId); + + expect(message.senderId).toBe(senderId); + expect(message.timestamp).toBeDefined(); + expect(message.protocolVersion).toBeDefined(); + }); + + it('should validate message integrity', () => { + const message = MessageBuilder.taskAssigned( + 'sender-1', + 'recipient-1', + { + taskId: 'task-1', + description: 'Test task', + githubIssue: 'https://github.com/org/repo/issues/1', + } + ); + + // Message should have all required fields + expect(message.protocolVersion).toBeDefined(); + expect(message.messageType).toBe(MessageType.TASK_ASSIGNED); + expect(message.senderId).toBe('sender-1'); + expect(message.recipientId).toBe('recipient-1'); + expect(message.timestamp).toBeDefined(); + }); + + it('should prevent message tampering detection', () => { + const message = MessageBuilder.heartbeat('agent-1'); + const originalTimestamp = message.timestamp; + + // Simulate tampering + const tamperedMessage = { + ...message, + timestamp: new Date(Date.now() + 1000000).toISOString(), + }; + + // Original timestamp should differ from tampered + expect(tamperedMessage.timestamp).not.toBe(originalTimestamp); + }); + }); + + describe('Token Security Best Practices', () => { + it('should generate tokens with sufficient entropy', () => { + // Generate multiple tokens and ensure uniqueness + const tokens = new Set(); + const count = 100; + + for (let i = 0; i < count; i++) { + tokens.add(generateChannelToken()); + } + + // All tokens should be unique + expect(tokens.size).toBe(count); + }); + + it('should support different token encodings', () => { + const hexToken = TokenGenerator.generate({ encoding: 'hex' }); + const base64Token = TokenGenerator.generate({ encoding: 'base64' }); + const base64urlToken = TokenGenerator.generate({ encoding: 'base64url' }); + + expect(hexToken).toMatch(/^[a-f0-9]+$/); + expect(base64Token).toMatch(/^[A-Za-z0-9+/=]+$/); + expect(base64urlToken).toMatch(/^[A-Za-z0-9_-]+$/); + }); + + it('should generate nonces for replay protection', () => { + const nonce1 = TokenGenerator.generateNonce(); + const nonce2 = TokenGenerator.generateNonce(); + + expect(nonce1).not.toBe(nonce2); + expect(nonce1).toHaveLength(32); // 16 bytes = 32 hex chars + }); + + it('should generate batch tokens efficiently', () => { + const count = 50; + const tokens = TokenGenerator.generateBatch(count); + + expect(tokens).toHaveLength(count); + expect(new Set(tokens).size).toBe(count); // All unique + }); + }); + + describe('Encryption Support', () => { + it('should warn when using unencrypted connections', () => { + // This test verifies that channels log warnings for insecure connections + // Actual implementation is in channel-specific code + + const insecureUrls = [ + 'http://example.com/hub', // HTTP instead of HTTPS + 'redis://localhost:6379', // Redis without TLS + ]; + + // In production, these should trigger warnings + insecureUrls.forEach(url => { + expect(url.startsWith('https://') || url.startsWith('rediss://')).toBe(false); + }); + }); + + it('should support secure connection URLs', () => { + const secureUrls = [ + 'https://example.com/hub', // HTTPS + 'rediss://localhost:6379', // Redis with TLS + 'wss://example.com/ws', // WebSocket Secure + ]; + + secureUrls.forEach(url => { + const isSecure = url.startsWith('https://') || + url.startsWith('rediss://') || + url.startsWith('wss://'); + expect(isSecure).toBe(true); + }); + }); + }); + + describe('Authentication Edge Cases', () => { + it('should handle null/undefined tokens gracefully', () => { + expect(() => { + TokenGenerator.validateFormat(null as any); + }).not.toThrow(); + + expect(TokenGenerator.validateFormat(undefined as any)).toBe(false); + }); + + it('should reject tokens with whitespace', () => { + const tokensWithWhitespace = [ + 'token with spaces', + 'token\twith\ttabs', + 'token\nwith\nnewlines', + ' leading-space', + 'trailing-space ', + ]; + + tokensWithWhitespace.forEach(token => { + expect(TokenGenerator.validateFormat(token)).toBe(false); + }); + }); + + it('should handle very long tokens', () => { + const longToken = 'a'.repeat(1000); + const hash = TokenGenerator.hash(longToken); + + expect(hash).toHaveLength(64); // SHA-256 always produces same length + }); + }); + + describe('Security Headers and Metadata', () => { + it('should include security-relevant fields in messages', () => { + const message = MessageBuilder.error( + 'agent-1', + { + code: 'AUTH_FAILED', + message: 'Authentication failed', + } + ); + + // Security-relevant fields should be present + expect(message.senderId).toBeDefined(); + expect(message.timestamp).toBeDefined(); + expect(message.protocolVersion).toBeDefined(); + expect(message.messageType).toBe(MessageType.ERROR); + expect(message.priority).toBeDefined(); + }); + + it('should support correlation IDs for request tracking', () => { + const correlationId = TokenGenerator.generateNonce(); + const query = MessageBuilder.statusQuery('agent-1', null, correlationId); + + expect(query.correlationId).toBe(correlationId); + }); + }); +}); diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 0000000..28fd0ee --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "node", + "resolveJsonModule": true, + "allowJs": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/relay-server/CoorChat.RelayServer.sln b/packages/relay-server/CoorChat.RelayServer.sln new file mode 100644 index 0000000..4cfdc57 --- /dev/null +++ b/packages/relay-server/CoorChat.RelayServer.sln @@ -0,0 +1,54 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8E3F9E7A-1234-4567-89AB-CDEF01234567}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9F4EADEF-5678-9ABC-DEF0-123456789ABC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoorChat.RelayServer.Api", "src\CoorChat.RelayServer.Api\CoorChat.RelayServer.Api.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoorChat.RelayServer.Core", "src\CoorChat.RelayServer.Core\CoorChat.RelayServer.Core.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoorChat.RelayServer.Data", "src\CoorChat.RelayServer.Data\CoorChat.RelayServer.Data.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoorChat.RelayServer.Tests.Unit", "tests\CoorChat.RelayServer.Tests.Unit\CoorChat.RelayServer.Tests.Unit.csproj", "{D4E5F6A7-B8C9-0123-DEF1-234567890123}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoorChat.RelayServer.Tests.Integration", "tests\CoorChat.RelayServer.Tests.Integration\CoorChat.RelayServer.Tests.Integration.csproj", "{E5F6A7B8-C9D0-1234-EF12-345678901234}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEF1-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-1234-EF12-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8E3F9E7A-1234-4567-89AB-CDEF01234567} + {B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {8E3F9E7A-1234-4567-89AB-CDEF01234567} + {C3D4E5F6-A7B8-9012-CDEF-123456789012} = {8E3F9E7A-1234-4567-89AB-CDEF01234567} + {D4E5F6A7-B8C9-0123-DEF1-234567890123} = {9F4EADEF-5678-9ABC-DEF0-123456789ABC} + {E5F6A7B8-C9D0-1234-EF12-345678901234} = {9F4EADEF-5678-9ABC-DEF0-123456789ABC} + EndGlobalSection +EndGlobal diff --git a/packages/relay-server/Dockerfile b/packages/relay-server/Dockerfile new file mode 100644 index 0000000..ab02b0a --- /dev/null +++ b/packages/relay-server/Dockerfile @@ -0,0 +1,47 @@ +# Multi-stage build for CoorChat Relay Server +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build +WORKDIR /src + +# Copy solution and project files +COPY CoorChat.RelayServer.sln ./ +COPY src/CoorChat.RelayServer.Api/CoorChat.RelayServer.Api.csproj src/CoorChat.RelayServer.Api/ +COPY src/CoorChat.RelayServer.Core/CoorChat.RelayServer.Core.csproj src/CoorChat.RelayServer.Core/ +COPY src/CoorChat.RelayServer.Data/CoorChat.RelayServer.Data.csproj src/CoorChat.RelayServer.Data/ +COPY tests/CoorChat.RelayServer.Tests.Unit/CoorChat.RelayServer.Tests.Unit.csproj tests/CoorChat.RelayServer.Tests.Unit/ +COPY tests/CoorChat.RelayServer.Tests.Integration/CoorChat.RelayServer.Tests.Integration.csproj tests/CoorChat.RelayServer.Tests.Integration/ + +# Restore dependencies +RUN dotnet restore + +# Copy source code +COPY src/ src/ +COPY tests/ tests/ + +# Build and publish +WORKDIR /src/src/CoorChat.RelayServer.Api +RUN dotnet build -c Release --no-restore && \ + dotnet publish -c Release -o /app/publish --no-build + +# Runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 -S coorchat && \ + adduser -S coorchat -u 1001 && \ + chown -R coorchat:coorchat /app + +USER coorchat + +# Copy published application +COPY --from=build /app/publish . + +# Expose ports +EXPOSE 80 +EXPOSE 443 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80/health || exit 1 + +ENTRYPOINT ["dotnet", "CoorChat.RelayServer.Api.dll"] diff --git a/packages/relay-server/src/CoorChat.RelayServer.Api/CoorChat.RelayServer.Api.csproj b/packages/relay-server/src/CoorChat.RelayServer.Api/CoorChat.RelayServer.Api.csproj new file mode 100644 index 0000000..e3bafb0 --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Api/CoorChat.RelayServer.Api.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + Linux + CoorChat.RelayServer.Api + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/packages/relay-server/src/CoorChat.RelayServer.Api/Middleware/AuthenticationMiddleware.cs b/packages/relay-server/src/CoorChat.RelayServer.Api/Middleware/AuthenticationMiddleware.cs new file mode 100644 index 0000000..4337cbf --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Api/Middleware/AuthenticationMiddleware.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Http; +using CoorChat.RelayServer.Core.Services; +using System.Threading.Tasks; + +namespace CoorChat.RelayServer.Api.Middleware +{ + /// + /// Authentication middleware for validating shared tokens + /// + public class AuthenticationMiddleware + { + private readonly RequestDelegate _next; + private readonly IAuthenticationService _authService; + + public AuthenticationMiddleware( + RequestDelegate next, + IAuthenticationService authService) + { + _next = next; + _authService = authService; + } + + public async Task InvokeAsync(HttpContext context) + { + // Skip authentication for health check endpoint + if (context.Request.Path.StartsWithSegments("/health")) + { + await _next(context); + return; + } + + // Extract token from Authorization header + var authHeader = context.Request.Headers["Authorization"].ToString(); + if (string.IsNullOrEmpty(authHeader)) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync("Missing Authorization header"); + return; + } + + // Parse Bearer token + var token = authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) + ? authHeader.Substring("Bearer ".Length).Trim() + : authHeader; + + // Validate token + var isValid = await _authService.ValidateTokenAsync(token); + if (!isValid) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync("Invalid authentication token"); + return; + } + + // Store token in context for downstream use + context.Items["AuthToken"] = token; + + // Continue to next middleware + await _next(context); + } + } + + /// + /// Extension methods for AuthenticationMiddleware + /// + public static class AuthenticationMiddlewareExtensions + { + public static IApplicationBuilder UseTokenAuthentication( + this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/packages/relay-server/src/CoorChat.RelayServer.Api/Program.cs b/packages/relay-server/src/CoorChat.RelayServer.Api/Program.cs new file mode 100644 index 0000000..04cd2ca --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Api/Program.cs @@ -0,0 +1,22 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container +builder.Services.AddControllers(); +builder.Services.AddSignalR(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/packages/relay-server/src/CoorChat.RelayServer.Core/CoorChat.RelayServer.Core.csproj b/packages/relay-server/src/CoorChat.RelayServer.Core/CoorChat.RelayServer.Core.csproj new file mode 100644 index 0000000..bb445bd --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Core/CoorChat.RelayServer.Core.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + CoorChat.RelayServer.Core + + + + + + + diff --git a/packages/relay-server/src/CoorChat.RelayServer.Core/Services/AuthenticationService.cs b/packages/relay-server/src/CoorChat.RelayServer.Core/Services/AuthenticationService.cs new file mode 100644 index 0000000..c2f0cbb --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Core/Services/AuthenticationService.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CoorChat.RelayServer.Core.Services +{ + /// + /// Authentication service for validating shared tokens + /// + public class AuthenticationService : IAuthenticationService + { + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly string _validTokenHash; + + public AuthenticationService( + IConfiguration configuration, + ILogger logger) + { + _configuration = configuration; + _logger = logger; + + // Load valid token from configuration + var validToken = _configuration["Authentication:SharedToken"] + ?? throw new InvalidOperationException("Authentication:SharedToken not configured"); + + // Store hash of valid token for timing-safe comparison + _validTokenHash = HashToken(validToken); + + _logger.LogInformation("Authentication service initialized"); + } + + /// + /// Validate an authentication token + /// + public Task ValidateTokenAsync(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + _logger.LogWarning("Empty token provided"); + return Task.FromResult(false); + } + + // Minimum token length check + if (token.Length < 16) + { + _logger.LogWarning("Token too short: {Length} characters", token.Length); + return Task.FromResult(false); + } + + try + { + // Hash provided token + var providedHash = HashToken(token); + + // Timing-safe comparison + var isValid = TimingSafeEqual(providedHash, _validTokenHash); + + if (!isValid) + { + _logger.LogWarning("Invalid token provided"); + } + + return Task.FromResult(isValid); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating token"); + return Task.FromResult(false); + } + } + + /// + /// Generate a new authentication token + /// + public string GenerateToken() + { + // Generate 32 bytes (256 bits) of random data + var bytes = new byte[32]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(bytes); + } + + // Convert to hex string with prefix + return "cct_" + Convert.ToHexString(bytes).ToLowerInvariant(); + } + + /// + /// Hash a token for secure storage + /// + public string HashToken(string token) + { + using var sha256 = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(token); + var hash = sha256.ComputeHash(bytes); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + /// + /// Timing-safe string comparison + /// + private bool TimingSafeEqual(string a, string b) + { + if (a == null || b == null) + return false; + + if (a.Length != b.Length) + return false; + + var result = 0; + for (int i = 0; i < a.Length; i++) + { + result |= a[i] ^ b[i]; + } + + return result == 0; + } + } +} diff --git a/packages/relay-server/src/CoorChat.RelayServer.Core/Services/IAuthenticationService.cs b/packages/relay-server/src/CoorChat.RelayServer.Core/Services/IAuthenticationService.cs new file mode 100644 index 0000000..c414e21 --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Core/Services/IAuthenticationService.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace CoorChat.RelayServer.Core.Services +{ + /// + /// Interface for authentication service + /// + public interface IAuthenticationService + { + /// + /// Validate an authentication token + /// + /// Token to validate + /// True if token is valid, false otherwise + Task ValidateTokenAsync(string token); + + /// + /// Generate a new authentication token + /// + /// New authentication token + string GenerateToken(); + + /// + /// Hash a token for secure storage + /// + /// Token to hash + /// Hashed token + string HashToken(string token); + } +} diff --git a/packages/relay-server/src/CoorChat.RelayServer.Data/CoorChat.RelayServer.Data.csproj b/packages/relay-server/src/CoorChat.RelayServer.Data/CoorChat.RelayServer.Data.csproj new file mode 100644 index 0000000..32e9830 --- /dev/null +++ b/packages/relay-server/src/CoorChat.RelayServer.Data/CoorChat.RelayServer.Data.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + CoorChat.RelayServer.Data + + + + + + + + + + + + + diff --git a/packages/relay-server/tests/CoorChat.RelayServer.Tests.Integration/CoorChat.RelayServer.Tests.Integration.csproj b/packages/relay-server/tests/CoorChat.RelayServer.Tests.Integration/CoorChat.RelayServer.Tests.Integration.csproj new file mode 100644 index 0000000..dde4c65 --- /dev/null +++ b/packages/relay-server/tests/CoorChat.RelayServer.Tests.Integration/CoorChat.RelayServer.Tests.Integration.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + false + true + CoorChat.RelayServer.Tests.Integration + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/packages/relay-server/tests/CoorChat.RelayServer.Tests.Unit/CoorChat.RelayServer.Tests.Unit.csproj b/packages/relay-server/tests/CoorChat.RelayServer.Tests.Unit/CoorChat.RelayServer.Tests.Unit.csproj new file mode 100644 index 0000000..7e9e038 --- /dev/null +++ b/packages/relay-server/tests/CoorChat.RelayServer.Tests.Unit/CoorChat.RelayServer.Tests.Unit.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + false + true + CoorChat.RelayServer.Tests.Unit + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/quick-start.ps1 b/quick-start.ps1 new file mode 100644 index 0000000..7475dad --- /dev/null +++ b/quick-start.ps1 @@ -0,0 +1,229 @@ +# CoorChat Quick Start Script (PowerShell) +# Automates local installation and setup + +$ErrorActionPreference = "Stop" + +Write-Host "πŸš€ CoorChat Quick Start" -ForegroundColor Cyan +Write-Host "=======================" -ForegroundColor Cyan +Write-Host "" + +# Check prerequisites +Write-Host "πŸ“‹ Checking prerequisites..." -ForegroundColor Yellow + +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Host "❌ Node.js not found. Please install Node.js 18+" -ForegroundColor Red + exit 1 +} + +if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + Write-Host "❌ npm not found. Please install npm" -ForegroundColor Red + exit 1 +} + +$dockerAvailable = $false +if (Get-Command docker -ErrorAction SilentlyContinue) { + $dockerAvailable = $true +} else { + Write-Host "⚠️ Docker not found. You'll need to install Redis manually." -ForegroundColor Yellow +} + +Write-Host "βœ… Prerequisites checked" -ForegroundColor Green +Write-Host "" + +# Navigate to MCP server +Set-Location packages\mcp-server + +# Install dependencies +Write-Host "πŸ“¦ Installing dependencies..." -ForegroundColor Yellow +npm install +Write-Host "βœ… Dependencies installed" -ForegroundColor Green +Write-Host "" + +# Build project +Write-Host "πŸ”¨ Building project..." -ForegroundColor Yellow +npm run build +Write-Host "βœ… Project built" -ForegroundColor Green +Write-Host "" + +# Generate secure token +Write-Host "πŸ”‘ Generating secure token..." -ForegroundColor Yellow +$TOKEN = node -e "console.log('cct_' + require('crypto').randomBytes(32).toString('hex'))" +Write-Host "βœ… Token generated" -ForegroundColor Green +Write-Host "" +Write-Host "Your secure token: " -NoNewline +Write-Host $TOKEN -ForegroundColor Yellow +Write-Host "⚠️ Save this token! You'll need it for all agents." -ForegroundColor Yellow +Write-Host "" + +# Save token to .env file +$envContent = @" +# CoorChat Configuration +# Generated: $(Get-Date) + +# Shared authentication token (use same token for all agents) +SHARED_TOKEN=$TOKEN + +# Channel configuration (redis, discord, or signalr) +CHANNEL_TYPE=redis +REDIS_HOST=localhost +REDIS_PORT=6379 + +# Agent configuration +AGENT_ID=agent-claude-1 +AGENT_ROLE=developer + +# Optional: GitHub integration +# GITHUB_TOKEN=ghp_your_token_here +# GITHUB_OWNER=your-org +# GITHUB_REPO=your-repo + +# Logging +LOG_LEVEL=info +"@ + +Set-Content -Path ".env" -Value $envContent +Write-Host "βœ… Configuration saved to .env" -ForegroundColor Green +Write-Host "" + +# Setup channel +Write-Host "πŸ“‘ Setting up communication channel..." -ForegroundColor Yellow +Write-Host "Choose your channel type:" +Write-Host " 1) Redis (Recommended - requires Docker)" +Write-Host " 2) Discord (Easy - requires Discord bot)" +Write-Host " 3) SignalR (Advanced - requires relay server)" +Write-Host "" +$channelChoice = Read-Host "Enter choice [1-3]" + +switch ($channelChoice) { + "1" { + if ($dockerAvailable) { + Write-Host "Starting Redis container..." -ForegroundColor Yellow + try { + docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine 2>$null + } catch { + Write-Host "Redis container already exists, starting it..." -ForegroundColor Yellow + docker start coorchat-redis + } + Write-Host "βœ… Redis started on localhost:6379" -ForegroundColor Green + } else { + Write-Host "❌ Docker not available. Please install Docker or choose another channel." -ForegroundColor Red + exit 1 + } + } + "2" { + Write-Host "" + Write-Host "Discord Setup Instructions:" -ForegroundColor Cyan + Write-Host "1. Go to https://discord.com/developers/applications" + Write-Host "2. Create a new application" + Write-Host "3. Go to 'Bot' β†’ 'Add Bot'" + Write-Host "4. Copy the bot token" + Write-Host "5. Enable 'Message Content Intent'" + Write-Host "6. Invite bot to your server" + Write-Host "7. Create a channel and copy its ID" + Write-Host "" + $discordToken = Read-Host "Enter Discord bot token" + $channelId = Read-Host "Enter Discord channel ID" + + # Update .env + $envContent = $envContent -replace "CHANNEL_TYPE=redis", "CHANNEL_TYPE=discord" + $envContent += "`nDISCORD_BOT_TOKEN=$discordToken" + $envContent += "`nDISCORD_CHANNEL_ID=$channelId" + Set-Content -Path ".env" -Value $envContent + Write-Host "βœ… Discord configured" -ForegroundColor Green + } + "3" { + Write-Host "Starting SignalR relay server..." -ForegroundColor Yellow + Set-Location ..\..\packages\relay-server + docker build -t coorchat-relay . + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to build relay server" -ForegroundColor Red + exit 1 + } + docker run -d --name coorchat-relay -p 5001:5001 -e "Authentication__SharedToken=$TOKEN" coorchat-relay + Set-Location ..\..\packages\mcp-server + + # Update .env + $envContent = $envContent -replace "CHANNEL_TYPE=redis", "CHANNEL_TYPE=signalr" + $envContent += "`nSIGNALR_HUB_URL=https://localhost:5001/agentHub" + Set-Content -Path ".env" -Value $envContent + Write-Host "βœ… SignalR relay server started" -ForegroundColor Green + } + default { + Write-Host "Invalid choice" -ForegroundColor Red + exit 1 + } +} + +Write-Host "" + +# Run tests +Write-Host "πŸ§ͺ Running tests..." -ForegroundColor Yellow +npm test -- --run +if ($LASTEXITCODE -ne 0) { + Write-Host "⚠️ Some tests failed, but installation is complete" -ForegroundColor Yellow +} +Write-Host "" + +# Generate Claude Desktop config +Write-Host "πŸ“ Generating Claude Desktop configuration..." -ForegroundColor Yellow + +$repoPath = (Get-Location).Path | Split-Path -Parent | Split-Path -Parent +$mcpServerPath = Join-Path $repoPath "packages\mcp-server\dist\index.js" +$mcpServerPath = $mcpServerPath -replace "\\", "\\" + +$claudeConfig = @" +{ + "mcpServers": { + "coorchat": { + "command": "node", + "args": [ + "$mcpServerPath" + ], + "env": { + "CHANNEL_TYPE": "redis", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "SHARED_TOKEN": "$TOKEN", + "AGENT_ID": "agent-claude-1", + "AGENT_ROLE": "developer", + "LOG_LEVEL": "info" + } + } + } +} +"@ + +Set-Content -Path "claude_desktop_config.json" -Value $claudeConfig +Write-Host "βœ… Claude Desktop config generated" -ForegroundColor Green +Write-Host "" + +# Installation complete +Write-Host "πŸŽ‰ Installation Complete!" -ForegroundColor Cyan +Write-Host "=======================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Next Steps:" -ForegroundColor Cyan +Write-Host "" +Write-Host "1. Copy the Claude Desktop configuration to:" +$claudeConfigPath = "$env:APPDATA\Claude\claude_desktop_config.json" +Write-Host " $claudeConfigPath" -ForegroundColor Yellow +Write-Host "" +Write-Host " Configuration file created at:" +Write-Host " .\claude_desktop_config.json" -ForegroundColor Yellow +Write-Host "" +Write-Host "2. Restart Claude Desktop" +Write-Host "" +Write-Host "3. Test the MCP server in Claude:" +Write-Host " Ask: 'Can you check if coorchat is connected?'" +Write-Host "" +Write-Host "Useful Commands:" -ForegroundColor Cyan +Write-Host "" +Write-Host " Start agents manually:" +Write-Host " npm run cli -- agent start --role developer" -ForegroundColor Yellow +Write-Host "" +Write-Host " Monitor coordination:" +Write-Host " npm run cli -- monitor" -ForegroundColor Yellow +Write-Host "" +Write-Host " View logs:" +Write-Host " npm run cli -- logs" -ForegroundColor Yellow +Write-Host "" +Write-Host "Happy coordinating! πŸ€–" -ForegroundColor Green diff --git a/quick-start.sh b/quick-start.sh new file mode 100644 index 0000000..485712f --- /dev/null +++ b/quick-start.sh @@ -0,0 +1,233 @@ +#!/bin/bash +# CoorChat Quick Start Script +# Automates local installation and setup + +set -e # Exit on error + +echo "πŸš€ CoorChat Quick Start" +echo "=======================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Check prerequisites +echo "πŸ“‹ Checking prerequisites..." + +if ! command -v node &> /dev/null; then + echo -e "${RED}❌ Node.js not found. Please install Node.js 18+${NC}" + exit 1 +fi + +if ! command -v npm &> /dev/null; then + echo -e "${RED}❌ npm not found. Please install npm${NC}" + exit 1 +fi + +if ! command -v docker &> /dev/null; then + echo -e "${YELLOW}⚠️ Docker not found. You'll need to install Redis manually.${NC}" + DOCKER_AVAILABLE=false +else + DOCKER_AVAILABLE=true +fi + +echo -e "${GREEN}βœ… Prerequisites checked${NC}" +echo "" + +# Navigate to MCP server +cd packages/mcp-server + +# Install dependencies +echo "πŸ“¦ Installing dependencies..." +npm install +echo -e "${GREEN}βœ… Dependencies installed${NC}" +echo "" + +# Build project +echo "πŸ”¨ Building project..." +npm run build +echo -e "${GREEN}βœ… Project built${NC}" +echo "" + +# Generate secure token +echo "πŸ”‘ Generating secure token..." +TOKEN=$(node -e "console.log('cct_' + require('crypto').randomBytes(32).toString('hex'))") +echo -e "${GREEN}βœ… Token generated${NC}" +echo "" +echo -e "${BLUE}Your secure token: ${YELLOW}${TOKEN}${NC}" +echo -e "${YELLOW}⚠️ Save this token! You'll need it for all agents.${NC}" +echo "" + +# Save token to .env file +cat > .env << EOF +# CoorChat Configuration +# Generated: $(date) + +# Shared authentication token (use same token for all agents) +SHARED_TOKEN=${TOKEN} + +# Channel configuration (redis, discord, or signalr) +CHANNEL_TYPE=redis +REDIS_HOST=localhost +REDIS_PORT=6379 + +# Agent configuration +AGENT_ID=agent-claude-1 +AGENT_ROLE=developer + +# Optional: GitHub integration +# GITHUB_TOKEN=ghp_your_token_here +# GITHUB_OWNER=your-org +# GITHUB_REPO=your-repo + +# Logging +LOG_LEVEL=info +EOF + +echo -e "${GREEN}βœ… Configuration saved to .env${NC}" +echo "" + +# Setup channel +echo "πŸ“‘ Setting up communication channel..." +echo "Choose your channel type:" +echo " 1) Redis (Recommended - requires Docker)" +echo " 2) Discord (Easy - requires Discord bot)" +echo " 3) SignalR (Advanced - requires relay server)" +echo "" +read -p "Enter choice [1-3]: " channel_choice + +case $channel_choice in + 1) + if [ "$DOCKER_AVAILABLE" = true ]; then + echo "Starting Redis container..." + docker run -d --name coorchat-redis -p 6379:6379 redis:7-alpine 2>/dev/null || { + echo -e "${YELLOW}Redis container already exists, starting it...${NC}" + docker start coorchat-redis + } + echo -e "${GREEN}βœ… Redis started on localhost:6379${NC}" + else + echo -e "${RED}❌ Docker not available. Please install Docker or choose another channel.${NC}" + exit 1 + fi + ;; + 2) + echo "" + echo "Discord Setup Instructions:" + echo "1. Go to https://discord.com/developers/applications" + echo "2. Create a new application" + echo "3. Go to 'Bot' β†’ 'Add Bot'" + echo "4. Copy the bot token" + echo "5. Enable 'Message Content Intent'" + echo "6. Invite bot to your server" + echo "7. Create a channel and copy its ID" + echo "" + read -p "Enter Discord bot token: " discord_token + read -p "Enter Discord channel ID: " channel_id + + # Update .env + sed -i "s/CHANNEL_TYPE=redis/CHANNEL_TYPE=discord/" .env + echo "DISCORD_BOT_TOKEN=${discord_token}" >> .env + echo "DISCORD_CHANNEL_ID=${channel_id}" >> .env + echo -e "${GREEN}βœ… Discord configured${NC}" + ;; + 3) + echo "Starting SignalR relay server..." + cd ../../packages/relay-server + docker build -t coorchat-relay . || { + echo -e "${RED}❌ Failed to build relay server${NC}" + exit 1 + } + docker run -d --name coorchat-relay -p 5001:5001 \ + -e "Authentication__SharedToken=${TOKEN}" \ + coorchat-relay + cd ../../packages/mcp-server + + # Update .env + sed -i "s/CHANNEL_TYPE=redis/CHANNEL_TYPE=signalr/" .env + echo "SIGNALR_HUB_URL=https://localhost:5001/agentHub" >> .env + echo -e "${GREEN}βœ… SignalR relay server started${NC}" + ;; + *) + echo -e "${RED}Invalid choice${NC}" + exit 1 + ;; +esac + +echo "" + +# Run tests +echo "πŸ§ͺ Running tests..." +npm test -- --run || { + echo -e "${YELLOW}⚠️ Some tests failed, but installation is complete${NC}" +} +echo "" + +# Generate Claude Desktop config +echo "πŸ“ Generating Claude Desktop configuration..." + +REPO_PATH=$(cd ../.. && pwd) +MCP_SERVER_PATH="${REPO_PATH}/packages/mcp-server/dist/index.js" + +cat > claude_desktop_config.json << EOF +{ + "mcpServers": { + "coorchat": { + "command": "node", + "args": [ + "${MCP_SERVER_PATH}" + ], + "env": { + "CHANNEL_TYPE": "redis", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "SHARED_TOKEN": "${TOKEN}", + "AGENT_ID": "agent-claude-1", + "AGENT_ROLE": "developer", + "LOG_LEVEL": "info" + } + } + } +} +EOF + +echo -e "${GREEN}βœ… Claude Desktop config generated${NC}" +echo "" + +# Installation complete +echo "πŸŽ‰ Installation Complete!" +echo "=======================" +echo "" +echo -e "${BLUE}Next Steps:${NC}" +echo "" +echo "1. Copy the Claude Desktop configuration:" +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + CLAUDE_CONFIG_PATH="%APPDATA%\\Claude\\claude_desktop_config.json" +else + CLAUDE_CONFIG_PATH="~/.claude/claude_desktop_config.json" +fi +echo -e " ${YELLOW}${CLAUDE_CONFIG_PATH}${NC}" +echo "" +echo " Configuration file created at:" +echo -e " ${YELLOW}./claude_desktop_config.json${NC}" +echo "" +echo "2. Restart Claude Desktop" +echo "" +echo "3. Test the MCP server in Claude:" +echo " Ask: 'Can you check if coorchat is connected?'" +echo "" +echo -e "${BLUE}Useful Commands:${NC}" +echo "" +echo " Start agents manually:" +echo -e " ${YELLOW}npm run cli -- agent start --role developer${NC}" +echo "" +echo " Monitor coordination:" +echo -e " ${YELLOW}npm run cli -- monitor${NC}" +echo "" +echo " View logs:" +echo -e " ${YELLOW}npm run cli -- logs${NC}" +echo "" +echo -e "${GREEN}Happy coordinating! πŸ€–${NC}" diff --git a/specs/001-multi-agent-coordination/checklists/requirements.md b/specs/001-multi-agent-coordination/checklists/requirements.md new file mode 100644 index 0000000..38d7914 --- /dev/null +++ b/specs/001-multi-agent-coordination/checklists/requirements.md @@ -0,0 +1,48 @@ +# Specification Quality Checklist: Multi-Agent Coordination System + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-02-14 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- **RESOLVED**: FR-004 clarification resolved - system will support all three channel types (Discord, SignalR, Redis cache) with pluggable architecture +- **ADDED**: Cross-platform support requirements (FR-016 through FR-021) for Linux, macOS, Windows, and CI/CD environments +- **ADDED**: Agent capability awareness and discovery requirements (FR-018, FR-19) +- **ADDED**: Structured message protocol requirements (FR-023) for standardized agent communication +- **ADDED**: Agent capability registration protocol (FR-024) for autonomous agent onboarding +- **ADDED**: Task lifecycle events (FR-25) for coordinated task management +- **ADDED**: MCP command interface with visual feedback (FR-026) +- **ADDED**: Extensible agent roles (FR-001, FR-027) - users can define custom agent types on the fly +- **ADDED**: New User Story 5: Agent Onboarding and Self-Management (Priority P2) +- **ADDED**: Agent Capability entity for structured capability metadata +- All checklist items now pass validation +- Specification is well-structured with clear user stories, prioritization, and testable requirements +- Success criteria are appropriately technology-agnostic and measurable +- Pluggable architecture supports future extensibility for additional channels and plugins +- Agent roles are now extensible - not limited to predefined list diff --git a/specs/001-multi-agent-coordination/contracts/capability-schema.json b/specs/001-multi-agent-coordination/contracts/capability-schema.json new file mode 100644 index 0000000..e48e616 --- /dev/null +++ b/specs/001-multi-agent-coordination/contracts/capability-schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://coorchat.dev/schemas/capability/v1.0.json", + "title": "Agent Capability Schema", + "description": "JSON Schema for agent capability registration and discovery", + "type": "object", + "required": ["agentId", "roleType", "platform", "tools"], + "properties": { + "agentId": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the agent" + }, + "roleType": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Agent role (extensible: developer, tester, architect, or custom)", + "examples": ["developer", "tester", "architect", "frontend", "backend", "infrastructure", "security-auditor", "documentation-writer"] + }, + "platform": { + "type": "string", + "enum": ["Linux", "macOS", "Windows"], + "description": "Operating system platform" + }, + "environmentType": { + "type": "string", + "description": "Execution environment", + "examples": ["local", "GitHub Actions", "Azure DevOps", "AWS CodeBuild", "GitLab CI", "CircleCI"] + }, + "tools": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "minItems": 1, + "description": "Available commands, CLIs, or APIs the agent can use", + "examples": [["git", "npm", "docker"], ["pytest", "jest", "selenium"], ["aws-cli", "terraform", "kubectl"]] + }, + "languages": { + "type": "array", + "items": { + "type": "string", + "maxLength": 50 + }, + "description": "Programming languages the agent can work with", + "examples": [["TypeScript", "JavaScript", "Python"], ["C#", "Java", "Go"]] + }, + "apiAccess": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "External APIs the agent has access to", + "examples": [["GitHub API", "Stripe API", "Twilio API"], ["OpenAI API", "Anthropic API"]] + }, + "resourceLimits": { + "$ref": "#/definitions/resourceLimits" + }, + "customMetadata": { + "type": "object", + "additionalProperties": true, + "description": "Custom capability metadata for specialized agent types" + } + }, + "definitions": { + "resourceLimits": { + "type": "object", + "description": "Resource constraints and quotas for the agent", + "properties": { + "apiQuotaPerHour": { + "type": "integer", + "minimum": 0, + "description": "Maximum API calls per hour" + }, + "maxConcurrentTasks": { + "type": "integer", + "minimum": 1, + "maximum": 10, + "default": 1, + "description": "Maximum number of simultaneous tasks" + }, + "rateLimitPerMinute": { + "type": "integer", + "minimum": 0, + "description": "Maximum requests per minute" + }, + "memoryLimitMB": { + "type": "integer", + "minimum": 0, + "description": "Memory constraint in megabytes" + } + } + } + }, + "examples": [ + { + "agentId": "550e8400-e29b-41d4-a716-446655440000", + "roleType": "developer", + "platform": "Linux", + "environmentType": "GitHub Actions", + "tools": ["git", "npm", "docker", "typescript", "jest"], + "languages": ["TypeScript", "JavaScript", "Python"], + "apiAccess": ["GitHub API", "npm Registry"], + "resourceLimits": { + "apiQuotaPerHour": 5000, + "maxConcurrentTasks": 2, + "rateLimitPerMinute": 60, + "memoryLimitMB": 2048 + } + }, + { + "agentId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", + "roleType": "tester", + "platform": "Windows", + "environmentType": "local", + "tools": ["playwright", "jest", "selenium"], + "languages": ["JavaScript", "TypeScript"], + "apiAccess": ["BrowserStack API"], + "resourceLimits": { + "apiQuotaPerHour": 1000, + "maxConcurrentTasks": 3, + "rateLimitPerMinute": 30, + "memoryLimitMB": 4096 + }, + "customMetadata": { + "browserSupport": ["chrome", "firefox", "safari"], + "testTypes": ["unit", "integration", "e2e"] + } + } + ] +} diff --git a/specs/001-multi-agent-coordination/contracts/mcp-commands.yaml b/specs/001-multi-agent-coordination/contracts/mcp-commands.yaml new file mode 100644 index 0000000..0d5da6b --- /dev/null +++ b/specs/001-multi-agent-coordination/contracts/mcp-commands.yaml @@ -0,0 +1,291 @@ +# MCP Commands Specification +# CoorChat Multi-Agent Coordination System + +version: "1.0" +description: "MCP command interface for configuring and managing the CoorChat coordination system" + +commands: + configure: + description: "Initialize or update channel configuration" + usage: "/coorchat configure [--channel-type ] [--retention ]" + parameters: + - name: channel-type + type: enum + required: false + values: [discord, signalr, redis, relay] + description: "Channel type to configure (prompts if not provided)" + + - name: retention + type: integer + required: false + default: 30 + description: "Message history retention in days" + + - name: config-file + type: string + required: false + default: ".coorchat/config.json" + description: "Path to configuration file" + + interactive: true + prompts: + - step: 1 + message: "Select channel type:" + options: + - value: discord + label: "Discord" + description: "Use Discord for coordination (requires bot token)" + - value: signalr + label: "SignalR" + description: "Use SignalR hub (requires hub URL)" + - value: redis + label: "Redis" + description: "Use Redis pub/sub (requires Redis connection)" + - value: relay + label: "CoorChat Relay Server" + description: "Use self-hosted relay server" + + - step: 2 + message: "Enter channel connection details:" + conditional: true + fields: + discord: + - name: bot-token + prompt: "Discord Bot Token" + env-var: "DISCORD_BOT_TOKEN" + secret: true + - name: guild-id + prompt: "Discord Guild (Server) ID" + - name: channel-id + prompt: "Discord Channel ID" + + signalr: + - name: hub-url + prompt: "SignalR Hub URL" + example: "https://your-server.com/agenthub" + - name: access-token + prompt: "Access Token" + env-var: "SIGNALR_TOKEN" + secret: true + + redis: + - name: host + prompt: "Redis Host" + default: "localhost" + - name: port + prompt: "Redis Port" + default: 6379 + - name: password + prompt: "Redis Password (optional)" + env-var: "REDIS_PASSWORD" + secret: true + optional: true + - name: channel-name + prompt: "Redis Pub/Sub Channel Name" + default: "coorchat" + + relay: + - name: server-url + prompt: "Relay Server URL" + example: "https://relay.coorchat.dev" + - name: channel-id + prompt: "Channel ID" + - name: access-token + prompt: "Access Token" + env-var: "RELAY_TOKEN" + secret: true + + - step: 3 + message: "Enter GitHub repository details:" + fields: + - name: github-token + prompt: "GitHub Personal Access Token" + env-var: "GITHUB_TOKEN" + secret: true + - name: repository-url + prompt: "Repository URL" + example: "https://github.com/owner/repo" + - name: webhook-secret + prompt: "Webhook Secret (optional, for webhook mode)" + env-var: "GITHUB_WEBHOOK_SECRET" + secret: true + optional: true + + - step: 4 + message: "Configure retention and logging:" + fields: + - name: retention-days + prompt: "Message retention (days)" + default: 30 + - name: log-level + prompt: "Log level" + options: [ERROR, WARN, INFO, DEBUG] + default: INFO + + output: + success: | + βœ… Configuration saved to {config-file} + + Channel: {channel-type} + Retention: {retention-days} days + Log Level: {log-level} + + Next: Run `/coorchat join` to connect an agent + + error: | + ❌ Configuration failed: {error-message} + + join: + description: "Join an agent to the coordination channel" + usage: "/coorchat join [--role ] [--agent-id ]" + parameters: + - name: role + type: string + required: false + description: "Agent role type (prompts if not provided)" + + - name: agent-id + type: uuid + required: false + description: "Agent UUID (generates if not provided)" + + - name: capabilities + type: string + required: false + description: "Path to capabilities JSON file" + + interactive: true + prompts: + - step: 1 + message: "Select agent role:" + options: + - value: developer + label: "Developer" + - value: tester + label: "Tester" + - value: architect + label: "Architect" + - value: frontend + label: "Frontend Developer" + - value: backend + label: "Backend Developer" + - value: infrastructure + label: "Infrastructure Engineer" + - value: custom + label: "Custom Role" + prompt-for-value: true + + - step: 2 + message: "Agent capabilities will be auto-detected. Override?" + fields: + - name: override-capabilities + prompt: "Provide custom capabilities file path (or press Enter to skip)" + optional: true + + output: + success: | + βœ… Agent joined successfully + + Agent ID: {agent-id} + Role: {role} + Platform: {detected-platform} + Environment: {detected-environment} + + Connected to: {channel-type} channel + Status: Connected + + error: | + ❌ Failed to join channel: {error-message} + + status: + description: "Display current coordination channel status" + usage: "/coorchat status [--verbose]" + parameters: + - name: verbose + type: boolean + required: false + default: false + description: "Show detailed information" + + output: + format: "text-ui" + template: | + β”Œβ”€ CoorChat Status ──────────────────────────┐ + β”‚ Channel: {channel-type}/{channel-name} β”‚ + β”‚ Connected Agents: {agent-count} β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ ● {agent-1-role} [{platform-1}] Task#{n}β”‚ + β”‚ ● {agent-2-role} [{platform-2}] Idle β”‚ + β”‚ β—‹ {agent-3-role} [{platform-3}] Offline β”‚ + β”‚ ... β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ Active Tasks: {active-task-count} β”‚ + β”‚ Messages Today: {message-count} β”‚ + β”‚ Uptime: {uptime} β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + capabilities: + description: "List capabilities of connected agents" + usage: "/coorchat capabilities [--agent-id ]" + parameters: + - name: agent-id + type: uuid + required: false + description: "Show capabilities for specific agent (shows all if omitted)" + + output: + format: "table" + template: | + Agent Capabilities: + + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Agent ID β”‚ Role β”‚ Platform β”‚ Tools β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ 550e8400-... β”‚ developer β”‚ Linux β”‚ git, npm, dockerβ”‚ + β”‚ 6ba7b810-... β”‚ tester β”‚ Windows β”‚ jest, playwrightβ”‚ + β”‚ ... β”‚ ... β”‚ ... β”‚ ... β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + disconnect: + description: "Disconnect current agent from the channel" + usage: "/coorchat disconnect [--graceful]" + parameters: + - name: graceful + type: boolean + required: false + default: true + description: "Wait for pending tasks to complete" + + output: + success: | + βœ… Agent disconnected successfully + + Session Duration: {session-duration} + Tasks Completed: {tasks-completed} + Messages Sent: {messages-sent} + + error: | + ❌ Disconnect failed: {error-message} + +errors: + ERR_NO_CONFIG: + code: "ERR_NO_CONFIG" + message: "No configuration found. Run `/coorchat configure` first." + + ERR_INVALID_TOKEN: + code: "ERR_INVALID_TOKEN" + message: "Invalid authentication token. Check your configuration." + + ERR_CHANNEL_UNAVAILABLE: + code: "ERR_CHANNEL_UNAVAILABLE" + message: "Cannot connect to channel. Verify channel is accessible." + + ERR_DUPLICATE_AGENT: + code: "ERR_DUPLICATE_AGENT" + message: "Agent with this ID is already connected." + +visual_feedback: + loading_spinner: true + progress_bars: true + color_support: auto # auto-detect terminal color support + unicode_support: auto # auto-detect terminal unicode support diff --git a/specs/001-multi-agent-coordination/contracts/message-protocol.json b/specs/001-multi-agent-coordination/contracts/message-protocol.json new file mode 100644 index 0000000..e842307 --- /dev/null +++ b/specs/001-multi-agent-coordination/contracts/message-protocol.json @@ -0,0 +1,176 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://coorchat.dev/schemas/message-protocol/v1.0.json", + "title": "CoorChat Message Protocol", + "description": "JSON Schema for Multi-Agent Coordination System message format", + "type": "object", + "required": ["protocolVersion", "messageType", "senderId", "timestamp"], + "properties": { + "protocolVersion": { + "type": "string", + "pattern": "^\\d+\\.\\d+$", + "description": "Semantic version of the protocol (e.g., '1.0')", + "examples": ["1.0", "1.1", "2.0"] + }, + "messageType": { + "type": "string", + "enum": [ + "task_assigned", + "task_started", + "task_blocked", + "task_progress", + "task_completed", + "task_failed", + "capability_query", + "capability_response", + "status_query", + "status_response", + "error", + "heartbeat", + "agent_joined", + "agent_left" + ], + "description": "Type of message being sent" + }, + "senderId": { + "type": "string", + "format": "uuid", + "description": "UUID of the sending agent" + }, + "recipientId": { + "type": "string", + "format": "uuid", + "description": "UUID of the recipient agent (null for broadcast)", + "nullable": true + }, + "taskId": { + "type": "string", + "format": "uuid", + "description": "UUID of the associated task (if applicable)", + "nullable": true + }, + "priority": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "default": 5, + "description": "Message priority (0=lowest, 10=highest)" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when message was created" + }, + "correlationId": { + "type": "string", + "format": "uuid", + "description": "UUID for matching request/response pairs", + "nullable": true + }, + "payload": { + "type": "object", + "description": "Message-specific data (varies by messageType)", + "additionalProperties": true + }, + "deliveryStatus": { + "type": "string", + "enum": ["queued", "sending", "sent", "delivered", "acknowledged", "failed"], + "default": "queued", + "description": "Current delivery state of the message" + } + }, + "definitions": { + "taskAssignedPayload": { + "type": "object", + "required": ["taskId", "description", "githubIssue"], + "properties": { + "taskId": { "type": "string", "format": "uuid" }, + "description": { "type": "string", "maxLength": 500 }, + "dependencies": { + "type": "array", + "items": { "type": "string", "format": "uuid" } + }, + "githubIssue": { "type": "string", "format": "uri" } + } + }, + "taskProgressPayload": { + "type": "object", + "required": ["taskId", "percentComplete", "status"], + "properties": { + "taskId": { "type": "string", "format": "uuid" }, + "percentComplete": { "type": "integer", "minimum": 0, "maximum": 100 }, + "status": { "type": "string", "maxLength": 200 } + } + }, + "taskCompletedPayload": { + "type": "object", + "required": ["taskId", "result"], + "properties": { + "taskId": { "type": "string", "format": "uuid" }, + "result": { "type": "object" }, + "githubPR": { "type": "string", "format": "uri", "nullable": true } + } + }, + "taskFailedPayload": { + "type": "object", + "required": ["taskId", "error", "retryable"], + "properties": { + "taskId": { "type": "string", "format": "uuid" }, + "error": { "type": "string" }, + "retryable": { "type": "boolean" }, + "stackTrace": { "type": "string", "nullable": true } + } + }, + "capabilityResponsePayload": { + "type": "object", + "required": ["agentId", "roleType", "platform", "tools"], + "properties": { + "agentId": { "type": "string", "format": "uuid" }, + "roleType": { "type": "string" }, + "platform": { "type": "string" }, + "environmentType": { "type": "string" }, + "tools": { "type": "array", "items": { "type": "string" } }, + "languages": { "type": "array", "items": { "type": "string" } }, + "apiAccess": { "type": "array", "items": { "type": "string" } }, + "resourceLimits": { "$ref": "#/definitions/resourceLimits" } + } + }, + "resourceLimits": { + "type": "object", + "properties": { + "apiQuotaPerHour": { "type": "integer", "minimum": 0 }, + "maxConcurrentTasks": { "type": "integer", "minimum": 1 }, + "rateLimitPerMinute": { "type": "integer", "minimum": 0 }, + "memoryLimitMB": { "type": "integer", "minimum": 0 } + } + }, + "errorPayload": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { "type": "string" }, + "message": { "type": "string" }, + "details": { "type": "object", "nullable": true } + } + } + }, + "examples": [ + { + "protocolVersion": "1.0", + "messageType": "task_assigned", + "senderId": "550e8400-e29b-41d4-a716-446655440000", + "recipientId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", + "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "priority": 7, + "timestamp": "2026-02-14T10:30:00Z", + "correlationId": "123e4567-e89b-12d3-a456-426614174000", + "payload": { + "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "description": "Implement user authentication feature", + "dependencies": [], + "githubIssue": "https://github.com/org/repo/issues/42" + }, + "deliveryStatus": "queued" + } + ] +} diff --git a/specs/001-multi-agent-coordination/data-model.md b/specs/001-multi-agent-coordination/data-model.md new file mode 100644 index 0000000..3a49666 --- /dev/null +++ b/specs/001-multi-agent-coordination/data-model.md @@ -0,0 +1,492 @@ +# Data Model: Multi-Agent Coordination System + +**Feature**: 001-multi-agent-coordination +**Date**: 2026-02-14 +**Status**: Phase 1 Design + +## Overview + +This document defines the data entities, relationships, state transitions, and validation rules for the Multi-Agent Coordination System. + +--- + +## Core Entities + +### 1. Agent + +Represents a specialized AI agent participating in coordination. + +**Attributes**: +- `id` (string, UUID): Unique agent identifier +- `role` (string): Agent role type (extensible: developer, tester, architect, custom roles) +- `platform` (enum): Operating system (Linux | macOS | Windows) +- `environment` (string): Execution environment (local, GitHub Actions, Azure DevOps, AWS, etc.) +- `capabilities` (Capability): Agent capability set (see Capability entity) +- `status` (enum): Connection status (disconnected | connecting | connected) +- `currentTask` (string, optional): ID of currently assigned task +- `registeredAt` (timestamp): When agent joined the channel +- `lastSeenAt` (timestamp): Last activity timestamp + +**Validation Rules**: +- `id`: Must be UUID v4 format +- `role`: Non-empty string, max 50 characters +- `platform`: Must be one of defined enum values +- `environment`: Non-empty string, max 100 characters +- `status`: Must be valid enum value +- `registeredAt`, `lastSeenAt`: ISO 8601 timestamp + +**State Transitions**: +``` +disconnected β†’ connecting β†’ connected β†’ disconnected + ↑ ↓ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + (reconnection) +``` + +**Lifecycle**: +1. **Registration**: Agent sends capability registration message +2. **Connected**: Agent receives acknowledgment, can send/receive messages +3. **Active**: Agent claims tasks, sends status updates +4. **Disconnected**: Agent explicitly disconnects or times out (30s no heartbeat) + +--- + +### 2. Message + +Represents a structured communication between agents. + +**Attributes**: +- `protocolVersion` (string): Semantic version (e.g., "1.0") +- `messageType` (enum): Message category + - `task_assigned`, `task_started`, `task_blocked`, `task_progress`, `task_completed`, `task_failed` + - `capability_query`, `capability_response` + - `status_query`, `status_response` + - `error` +- `senderId` (string, UUID): Sending agent ID +- `recipientId` (string, UUID, optional): Recipient agent ID (null for broadcast) +- `taskId` (string, UUID, optional): Associated task ID (if applicable) +- `priority` (integer): Message priority (0-10, default 5) +- `timestamp` (timestamp): Message creation time +- `correlationId` (string, UUID, optional): For request/response matching +- `payload` (object): Message-specific data +- `deliveryStatus` (enum): queued | sending | sent | delivered | acknowledged | failed + +**Validation Rules**: +- `protocolVersion`: Must match pattern `^\d+\.\d+$` +- `messageType`: Must be valid enum value +- `senderId`: Must be valid UUID +- `recipientId`: If present, must be valid UUID +- `taskId`: If present, must be valid UUID +- `priority`: Integer 0-10 +- `timestamp`: ISO 8601 timestamp +- `correlationId`: If present, must be valid UUID +- `payload`: Valid JSON object + +**State Transitions**: +``` +queued β†’ sending β†’ sent β†’ delivered β†’ acknowledged + ↓ + failed +``` + +**Message Types & Payloads**: + +```typescript +// task_assigned +payload: { + taskId: string; + description: string; + dependencies: string[]; + githubIssue: string; // Issue URL +} + +// task_started +payload: { + taskId: string; + estimatedCompletionTime: timestamp; +} + +// task_progress +payload: { + taskId: string; + percentComplete: number; // 0-100 + status: string; +} + +// task_completed +payload: { + taskId: string; + result: object; // Task-specific result data + githubPR: string; // PR URL if applicable +} + +// task_failed +payload: { + taskId: string; + error: string; + retryable: boolean; +} + +// capability_query +payload: { + // Empty or specific capability filter +} + +// capability_response +payload: { + capabilities: Capability; // See Capability entity +} + +// status_query +payload: { + agentId: string; // Query specific agent or broadcast +} + +// status_response +payload: { + currentTask: string | null; + status: string; + uptime: number; // seconds +} + +// error +payload: { + code: string; + message: string; + details: object; +} +``` + +--- + +### 3. Task + +Represents a work item from GitHub repository. + +**Attributes**: +- `id` (string, UUID): Unique task identifier +- `description` (string): Task description +- `assignedAgents` (string[]): Array of assigned agent IDs +- `status` (enum): Task state (available | assigned | started | in_progress | blocked | completed | failed) +- `dependencies` (string[]): Array of task IDs this task depends on +- `githubIssueId` (string): GitHub issue number +- `githubIssueUrl` (string): Full GitHub issue URL +- `githubPRUrl` (string, optional): Associated PR URL +- `createdAt` (timestamp): When task was created +- `assignedAt` (timestamp, optional): When task was assigned +- `startedAt` (timestamp, optional): When work started +- `completedAt` (timestamp, optional): When work finished +- `claimedAt` (timestamp, optional): Timestamp for conflict resolution + +**Validation Rules**: +- `id`: UUID v4 +- `description`: Non-empty, max 500 characters +- `assignedAgents`: Array of valid UUIDs +- `status`: Valid enum value +- `dependencies`: Array of valid task UUIDs +- `githubIssueId`: Integer as string +- `githubIssueUrl`: Valid URL +- All timestamps: ISO 8601 + +**State Transitions**: +``` +available β†’ assigned β†’ started β†’ in_progress β†’ completed + ↓ ↓ ↓ + └─→ blocked β†β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + ↓ + failed +``` + +**Conflict Resolution**: +When multiple agents claim same task simultaneously: +1. Compare `claimedAt` timestamps +2. Agent with earliest timestamp wins +3. Other agents receive `task_unavailable` error +4. Implement idempotency: deduplicate by correlation ID + +--- + +### 4. Channel + +Represents the coordination communication space. + +**Attributes**: +- `id` (string, UUID): Channel identifier +- `type` (enum): Channel type (discord | signalr | redis | relay) +- `name` (string): Human-readable channel name +- `participants` (string[]): Array of connected agent IDs +- `config` (ChannelConfig): Channel-specific configuration +- `securitySettings` (SecuritySettings): Auth token, encryption settings +- `messageHistory` (boolean): Whether history is persisted +- `retentionDays` (integer): Message retention period (configured at init) +- `createdAt` (timestamp): Channel creation time +- `createdBy` (string): User/admin who created channel + +**Validation Rules**: +- `id`: UUID v4 +- `type`: Valid enum value +- `name`: Non-empty, max 100 characters +- `participants`: Array of valid UUIDs +- `retentionDays`: Positive integer +- `createdAt`: ISO 8601 timestamp + +**Channel-Specific Config**: + +```typescript +// Discord +DiscordConfig: { + guildId: string; + channelId: string; + botToken: string; // Encrypted +} + +// SignalR +SignalRConfig: { + hubUrl: string; + accessToken: string; // Encrypted +} + +// Redis +RedisConfig: { + host: string; + port: number; + password: string; // Encrypted + db: number; + channelName: string; // Redis pub/sub channel +} + +// Relay +RelayConfig: { + serverUrl: string; + channelId: string; + accessToken: string; // Encrypted +} +``` + +**Security Settings**: +```typescript +SecuritySettings: { + sharedToken: string; // Self-generated, encrypted at rest + encryptionEnabled: boolean; + allowedAgentRoles: string[]; // Role-based access control +} +``` + +--- + +### 5. Capability + +Represents an agent's declared capabilities. + +**Attributes**: +- `agentId` (string, UUID): Associated agent ID +- `roleType` (string): Agent role (extensible) +- `platform` (string): OS platform +- `environmentType` (string): Execution environment +- `tools` (string[]): Available commands/APIs +- `languages` (string[]): Supported programming languages +- `apiAccess` (string[]): External APIs agent can call +- `resourceLimits` (ResourceLimits): Quota/rate limit info +- `customMetadata` (object): Custom capability fields + +**Validation Rules**: +- `agentId`: Valid UUID +- `roleType`: Non-empty string, max 50 characters +- Arrays: Non-empty, each item max 100 characters +- `resourceLimits`: Valid ResourceLimits object + +**Resource Limits**: +```typescript +ResourceLimits: { + apiQuotaPerHour: number; // Max API calls per hour + maxConcurrentTasks: number; // Max simultaneous tasks + rateLimitPerMinute: number; // Max requests per minute + memoryLimitMB: number; // Memory constraint +} +``` + +--- + +### 6. Configuration + +Represents system configuration (stored in local JSON/YAML files). + +**Attributes**: +- `version` (string): Config schema version +- `channel` (ChannelSettings): Channel configuration +- `github` (GitHubSettings): GitHub integration settings +- `logging` (LoggingSettings): Log level and outputs +- `advanced` (AdvancedSettings): Optional advanced settings + +**Validation Rules**: +- All settings validated by Zod schema +- Environment variable substitution: `${VAR_NAME}` syntax +- Secrets must use environment variables (not hardcoded) + +**Configuration Schema**: + +```typescript +Configuration: { + version: "1.0", + channel: { + type: "discord" | "signalr" | "redis" | "relay", + config: ChannelConfig, // Type-specific config + retentionDays: number, + token: string, // Can be ${CHANNEL_TOKEN} + }, + github: { + token: string, // ${GITHUB_TOKEN} + repositoryUrl: string, + webhookSecret: string | null, + pollingIntervalSeconds: number, // Default: 30 + }, + logging: { + level: "ERROR" | "WARN" | "INFO" | "DEBUG", + outputs: ("console" | "file")[], + filePath: string | null, + }, + advanced: { + maxRetries: number, // Default: 5 + connectionTimeoutMs: number, // Default: 30000 + heartbeatIntervalMs: number, // Default: 15000 + } +} +``` + +--- + +## Relationships + +### Entity Relationship Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Channel β”‚ +β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ 1:N + β”‚ +β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” +β”‚ Agent │───────────┐ +β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ + β”‚ 1:N β”‚ N:M + β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β” +β”‚ Message β”‚ β”‚ Task β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ N:M (dependencies) + └─────┐ + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ Task β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Capability β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ 1:1 + β”‚ + β”Œβ”€β”€β”€β”΄β”€β”€β”€β” + β”‚ Agent β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Configuration β”‚ (File-based, not in database) +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Cardinality + +- **Channel ↔ Agent**: 1:N (one channel, many agents) +- **Agent ↔ Message**: 1:N (one agent sends many messages) +- **Agent ↔ Task**: N:M (agents can have multiple tasks, tasks can have multiple agents) +- **Task ↔ Task**: N:M (task dependencies) +- **Agent ↔ Capability**: 1:1 (each agent has one capability set) +- **Channel ↔ Message**: 1:N (one channel, many messages in history) + +--- + +## Indexes & Performance + +### Recommended Database Indexes + +For Relay Server (PostgreSQL): + +```sql +-- Messages +CREATE INDEX idx_messages_channel_timestamp ON messages(channel_id, timestamp DESC); +CREATE INDEX idx_messages_timestamp ON messages(timestamp); -- For retention purge +CREATE INDEX idx_messages_task_id ON messages(task_id) WHERE task_id IS NOT NULL; + +-- Agents +CREATE INDEX idx_agents_channel_status ON agents(channel_id, status); +CREATE INDEX idx_agents_role ON agents(role); + +-- Tasks +CREATE INDEX idx_tasks_status ON tasks(status); +CREATE INDEX idx_tasks_assigned_agents ON tasks USING GIN(assigned_agents); -- Array index +``` + +### Caching Strategy + +- **Agent Capabilities**: Cache in-memory (TTL: 5 minutes) +- **Channel Participants**: Cache in-memory (invalidate on join/leave) +- **Task Status**: Cache with pub/sub invalidation (Redis if using Redis channel) + +--- + +## Data Volume Estimates + +**Assumptions** (per channel): +- 20-50 concurrent agents +- 10 messages/minute average (14,400 messages/day) +- 30-day retention policy +- Average message size: 1KB + +**Storage Requirements**: +- Messages: 14,400 messages/day Γ— 30 days Γ— 1KB = ~432MB/month +- Agents: 50 agents Γ— 2KB = 100KB (negligible) +- Tasks: 100 active tasks Γ— 5KB = 500KB (negligible) + +**Total: ~500MB per channel per month** (primarily message history) + +With purge policies and retention, storage stabilizes at ~500MB. + +--- + +## Security Considerations + +### Data Protection + +1. **Secrets**: Never store plaintext tokens/passwords + - Use environment variables + - Encrypt at rest in database (if Relay Server) + +2. **Message Encryption**: Channel-specific + - Discord: TLS in transit (Discord handles encryption) + - SignalR: TLS in transit + - Redis: TLS optional, configure redis-cli with `--tls` + - Relay Server: TLS mandatory + +3. **Access Control**: + - Shared token per channel (scoped to channel) + - Optional role-based access (filter by `allowedAgentRoles`) + +### Compliance + +- **Data Retention**: Configurable purge policies (GDPR compliance) +- **Audit Trail**: Message history with timestamps +- **PII**: Avoid storing personal data in messages (agent IDs are UUIDs, not names) + +--- + +## Phase 1 Design Complete + +Data model fully specified with: +- βœ… 6 core entities +- βœ… Validation rules +- βœ… State transitions +- βœ… Relationships (ERD) +- βœ… Performance indexes +- βœ… Security considerations + +**Next**: Generate API contracts (message-protocol.json, capability-schema.json, etc.) diff --git a/specs/001-multi-agent-coordination/plan.md b/specs/001-multi-agent-coordination/plan.md new file mode 100644 index 0000000..2ee90ef --- /dev/null +++ b/specs/001-multi-agent-coordination/plan.md @@ -0,0 +1,387 @@ +# Implementation Plan: Multi-Agent Coordination System + +**Branch**: `001-multi-agent-coordination` | **Date**: 2026-02-14 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-multi-agent-coordination/spec.md` + +## Summary + +Build a coordination system enabling multiple specialized AI agents (developer, tester, architect, frontend, backend, infrastructure, and custom roles) to collaborate on shared software development tasks in real-time. The system consists of two integrated components: + +1. **MCP Server** (TypeScript/Node.js): Agent-facing coordination client providing MCP commands, GitHub integration, and channel abstraction +2. **CoorChat Relay Server** (C#/.NET): Optional custom relay server providing authenticated communications, centralized message history, and configuration management + +Agents communicate through pluggable channels (Discord, SignalR, Redis, or CoorChat Relay) using a structured JSON protocol with versioning. The system supports cross-platform deployment (Linux/macOS/Windows), CI/CD pipeline execution, and autonomous agent onboarding with capability discovery. + +## Technical Context + +**Language/Version**: +- MCP Server: TypeScript 5.x / Node.js v18+ +- Relay Server: C# / .NET 8.0+ + +**Primary Dependencies**: +- MCP Server: Discord.js, @microsoft/signalr, ioredis, @octokit/rest (GitHub API), ws (WebSockets) +- Relay Server: ASP.NET Core, SignalR, Entity Framework Core + +**Storage**: +- Configuration: Local JSON/YAML files (`.coorchat/config.json`) +- Message History: Channel provider's native storage (Discord history, Redis persistence, Relay Server database) +- Relay Server: SQL database (PostgreSQL/SQL Server) for centralized storage + +**Testing**: +- MCP Server: Jest/Vitest for unit tests, Playwright for integration tests +- Relay Server: xUnit for unit tests, integration tests with TestContainers + +**Target Platform**: +- Linux (amd64, arm64), macOS, Windows +- Docker containers (primary distribution) +- npm packages (alternative distribution) +- CI/CD environments (GitHub Actions, Azure DevOps, AWS) + +**Project Type**: Multi-project (MCP Server + Relay Server) + +**Performance Goals**: +- Message latency: <2 seconds under normal network conditions +- Message delivery: 99.9% success rate +- Concurrent agents: 20-50 agents per channel +- GitHub sync: <5 seconds for work item updates +- Agent capability discovery: <5 seconds + +**Constraints**: +- Installation time: <5 minutes (including Docker pull) +- Configuration time: <2 minutes per agent via MCP commands +- Reconnection time: <30 seconds after unexpected disconnection +- Docker image size: <500MB (target) +- Memory per agent: <200MB (target) + +**Scale/Scope**: +- 20-50 concurrent agents per coordination channel +- 4 channel types supported (Discord, SignalR, Redis, CoorChat Relay) +- Cross-platform: 3 OS platforms Γ— multiple environments +- Extensible agent roles (unlimited custom types) + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +**Status**: No constitution file found at `.specify/memory/constitution.md` + +**Default Principles Applied**: +- βœ… Simplicity: Use pluggable architecture to avoid duplicating channel logic +- βœ… Testability: Structured protocol enables contract testing +- βœ… Maintainability: TypeScript provides type safety for message protocol +- ⚠️ Multi-project: Two separate components (MCP + Relay) required for different use cases + +**Multi-Project Justification**: +| Component | Language | Rationale | +|-----------|----------|-----------| +| MCP Server | TypeScript/Node.js | Real-time messaging excellence, JSON-native, excellent Discord/SignalR/Redis libraries, cross-platform | +| Relay Server | C#/.NET | Optional component for teams wanting self-hosted solution, .NET ecosystem integration, SignalR native support | + +**Re-evaluation after Phase 1**: Pending design completion + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-multi-agent-coordination/ +β”œβ”€β”€ spec.md # Feature specification +β”œβ”€β”€ plan.md # This file (/speckit.plan output) +β”œβ”€β”€ research.md # Phase 0 output (technology decisions) +β”œβ”€β”€ data-model.md # Phase 1 output (entities & protocol) +β”œβ”€β”€ quickstart.md # Phase 1 output (getting started guide) +β”œβ”€β”€ contracts/ # Phase 1 output (API contracts, protocol schemas) +β”‚ β”œβ”€β”€ message-protocol.json # JSON schema for message format +β”‚ β”œβ”€β”€ capability-schema.json # Agent capability registration format +β”‚ β”œβ”€β”€ mcp-commands.yaml # MCP command specifications +β”‚ └── relay-api.openapi.yaml # Relay Server API spec (if implemented) +β”œβ”€β”€ checklists/ +β”‚ └── requirements.md # Specification quality checklist +└── tasks.md # Phase 2 output (/speckit.tasks - not created by /speckit.plan) +``` + +### Source Code (repository root) + +```text +# MCP Server (TypeScript/Node.js) +packages/mcp-server/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ channels/ # Channel abstraction layer +β”‚ β”‚ β”œβ”€β”€ base/ +β”‚ β”‚ β”‚ β”œβ”€β”€ Channel.ts # Base channel interface +β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelFactory.ts # Factory for creating channels +β”‚ β”‚ β”‚ └── ChannelAdapter.ts # Common adapter logic +β”‚ β”‚ β”œβ”€β”€ discord/ +β”‚ β”‚ β”‚ β”œβ”€β”€ DiscordChannel.ts +β”‚ β”‚ β”‚ └── DiscordAdapter.ts +β”‚ β”‚ β”œβ”€β”€ signalr/ +β”‚ β”‚ β”‚ β”œβ”€β”€ SignalRChannel.ts +β”‚ β”‚ β”‚ └── SignalRAdapter.ts +β”‚ β”‚ β”œβ”€β”€ redis/ +β”‚ β”‚ β”‚ β”œβ”€β”€ RedisChannel.ts +β”‚ β”‚ β”‚ └── RedisAdapter.ts +β”‚ β”‚ └── relay/ +β”‚ β”‚ β”œβ”€β”€ RelayChannel.ts +β”‚ β”‚ └── RelayAdapter.ts +β”‚ β”œβ”€β”€ protocol/ # Message protocol implementation +β”‚ β”‚ β”œβ”€β”€ Message.ts # Message type definitions +β”‚ β”‚ β”œβ”€β”€ MessageBuilder.ts # Fluent message builder +β”‚ β”‚ β”œβ”€β”€ MessageValidator.ts # Protocol validation +β”‚ β”‚ └── VersionManager.ts # Protocol versioning +β”‚ β”œβ”€β”€ agents/ # Agent management +β”‚ β”‚ β”œβ”€β”€ Agent.ts # Agent entity +β”‚ β”‚ β”œβ”€β”€ AgentRegistry.ts # Agent tracking +β”‚ β”‚ β”œβ”€β”€ CapabilityManager.ts # Capability registration/discovery +β”‚ β”‚ └── RoleManager.ts # Custom role definitions +β”‚ β”œβ”€β”€ tasks/ # Task coordination +β”‚ β”‚ β”œβ”€β”€ Task.ts # Task entity +β”‚ β”‚ β”œβ”€β”€ TaskQueue.ts # Task assignment queue +β”‚ β”‚ β”œβ”€β”€ ConflictResolver.ts # Timestamp-based conflict resolution +β”‚ β”‚ └── DependencyTracker.ts # Task dependency management +β”‚ β”œβ”€β”€ github/ # GitHub integration +β”‚ β”‚ β”œβ”€β”€ GitHubClient.ts # GitHub API wrapper +β”‚ β”‚ β”œβ”€β”€ WebhookHandler.ts # Webhook receiver +β”‚ β”‚ β”œβ”€β”€ PollingService.ts # Fallback polling +β”‚ β”‚ └── SyncManager.ts # Work item synchronization +β”‚ β”œβ”€β”€ config/ # Configuration management +β”‚ β”‚ β”œβ”€β”€ ConfigLoader.ts # JSON/YAML config loader +β”‚ β”‚ β”œβ”€β”€ ConfigValidator.ts # Config validation +β”‚ β”‚ └── EnvironmentResolver.ts # Env var substitution +β”‚ β”œβ”€β”€ mcp/ # MCP command interface +β”‚ β”‚ β”œβ”€β”€ CommandHandler.ts # MCP command dispatcher +β”‚ β”‚ β”œβ”€β”€ commands/ +β”‚ β”‚ β”‚ β”œβ”€β”€ ConfigureCommand.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ JoinCommand.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ StatusCommand.ts +β”‚ β”‚ β”‚ └── CapabilitiesCommand.ts +β”‚ β”‚ └── ui/ +β”‚ β”‚ └── TextUI.ts # Text-based visual feedback +β”‚ β”œβ”€β”€ logging/ # Observability +β”‚ β”‚ β”œβ”€β”€ Logger.ts # Structured logger interface +β”‚ β”‚ β”œβ”€β”€ LogLevel.ts # Log level enum +β”‚ β”‚ └── LogFormatter.ts # Log formatting +β”‚ β”œβ”€β”€ retry/ # Rate limiting & retry +β”‚ β”‚ β”œβ”€β”€ RetryQueue.ts # Request queue +β”‚ β”‚ β”œβ”€β”€ ExponentialBackoff.ts # Backoff algorithm +β”‚ β”‚ └── RateLimiter.ts # API rate limiting +β”‚ └── index.ts # Main entry point +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ unit/ +β”‚ β”‚ β”œβ”€β”€ protocol/ +β”‚ β”‚ β”œβ”€β”€ agents/ +β”‚ β”‚ β”œβ”€β”€ tasks/ +β”‚ β”‚ └── channels/ +β”‚ β”œβ”€β”€ integration/ +β”‚ β”‚ β”œβ”€β”€ github-sync.test.ts +β”‚ β”‚ β”œβ”€β”€ channel-switching.test.ts +β”‚ β”‚ └── multi-agent.test.ts +β”‚ └── contract/ +β”‚ β”œβ”€β”€ message-protocol.test.ts +β”‚ └── capability-schema.test.ts +β”œβ”€β”€ package.json +β”œβ”€β”€ tsconfig.json +└── Dockerfile + +# CoorChat Relay Server (C#/.NET) - OPTIONAL COMPONENT +packages/relay-server/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ CoorChat.RelayServer.Api/ +β”‚ β”‚ β”œβ”€β”€ Controllers/ +β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelController.cs # Channel management API +β”‚ β”‚ β”‚ β”œβ”€β”€ MessageController.cs # Message relay API +β”‚ β”‚ β”‚ └── ConfigController.cs # Configuration API +β”‚ β”‚ β”œβ”€β”€ Hubs/ +β”‚ β”‚ β”‚ └── AgentHub.cs # SignalR hub for real-time +β”‚ β”‚ β”œβ”€β”€ Middleware/ +β”‚ β”‚ β”‚ β”œβ”€β”€ AuthenticationMiddleware.cs +β”‚ β”‚ β”‚ └── LoggingMiddleware.cs +β”‚ β”‚ β”œβ”€β”€ Program.cs +β”‚ β”‚ └── appsettings.json +β”‚ β”œβ”€β”€ CoorChat.RelayServer.Core/ +β”‚ β”‚ β”œβ”€β”€ Entities/ +β”‚ β”‚ β”‚ β”œβ”€β”€ Channel.cs +β”‚ β”‚ β”‚ β”œβ”€β”€ Message.cs +β”‚ β”‚ β”‚ β”œβ”€β”€ Agent.cs +β”‚ β”‚ β”‚ └── Configuration.cs +β”‚ β”‚ β”œβ”€β”€ Services/ +β”‚ β”‚ β”‚ β”œβ”€β”€ IMessageRelayService.cs +β”‚ β”‚ β”‚ β”œβ”€β”€ MessageRelayService.cs +β”‚ β”‚ β”‚ β”œβ”€β”€ IChannelService.cs +β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelService.cs +β”‚ β”‚ β”‚ └── AuthenticationService.cs +β”‚ β”‚ └── Interfaces/ +β”‚ └── CoorChat.RelayServer.Data/ +β”‚ β”œβ”€β”€ DbContext/ +β”‚ β”‚ └── RelayDbContext.cs +β”‚ β”œβ”€β”€ Repositories/ +β”‚ β”‚ β”œβ”€β”€ IChannelRepository.cs +β”‚ β”‚ β”œβ”€β”€ ChannelRepository.cs +β”‚ β”‚ β”œβ”€β”€ IMessageRepository.cs +β”‚ β”‚ └── MessageRepository.cs +β”‚ └── Migrations/ +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ CoorChat.RelayServer.Tests.Unit/ +β”‚ └── CoorChat.RelayServer.Tests.Integration/ +β”œβ”€β”€ CoorChat.RelayServer.sln +└── Dockerfile + +# Shared +.github/ +└── workflows/ + β”œβ”€β”€ mcp-server-ci.yml # MCP Server build/test/publish + └── relay-server-ci.yml # Relay Server build/test/publish + +docker-compose.yml # Local development setup +README.md # Repository documentation +``` + +**Structure Decision**: Multi-project monorepo with two independent components: +1. **MCP Server (TypeScript/Node.js)**: Primary coordination client under `packages/mcp-server/` +2. **Relay Server (C#/.NET)**: Optional self-hosted relay under `packages/relay-server/` + +This structure allows teams to use only the MCP Server with third-party channels (Discord/SignalR/Redis) or deploy both components for a fully self-hosted solution. + +## Complexity Tracking + +**No violations requiring justification**. The multi-project structure is necessary for: +- Different runtime environments (Node.js vs .NET) +- Optional deployment scenarios (MCP-only vs MCP+Relay) +- Language-specific ecosystem strengths (TypeScript for real-time, C# for SignalR native) + +--- + +## Phase 0: Research & Technology Decisions + +### Research Tasks + +Based on Technical Context, the following areas require research to validate technology choices: + +1. **Channel Abstraction Pattern**: Research best practices for multi-channel abstraction in TypeScript + - Strategy pattern vs Factory pattern vs Plugin architecture + - Ensure channel switching doesn't break existing connections + +2. **Message Protocol Design**: Research JSON schema versioning strategies + - Schema evolution patterns (add/remove fields) + - Backward compatibility testing approaches + - Protocol negotiation mechanisms + +3. **Real-time Performance**: Research Discord.js, SignalR client, and ioredis performance characteristics + - Concurrent connection limits + - Message throughput benchmarks + - Memory footprint per connection + +4. **GitHub Integration**: Research webhook reliability and polling fallback patterns + - Webhook delivery guarantees + - Polling optimization (conditional requests, ETags) + - Event deduplication strategies + +5. **Cross-platform Docker**: Research multi-platform Docker image builds + - GitHub Actions matrix builds for linux/amd64, linux/arm64, Windows + - Image size optimization techniques + - Platform-specific dependencies + +6. **Rate Limiting**: Research exponential backoff algorithms + - Standard backoff formulas (2^n, jitter) + - Circuit breaker patterns + - Rate limit header parsing (GitHub, Discord APIs) + +7. **Configuration Management**: Research secure configuration storage + - Environment variable substitution patterns + - Secret management best practices + - Configuration validation libraries (Joi, Zod) + +8. **Relay Server Storage**: Research Entity Framework Core with PostgreSQL/SQL Server + - Message retention/purging strategies + - Query performance for message history + - Connection pooling configuration + +### Research Output Location + +All research findings will be consolidated in `research.md` with decisions, rationales, and rejected alternatives. + +--- + +## Phase 1: Design & Contracts + +### Data Model (data-model.md) + +Based on spec entities, create detailed data model covering: + +**Core Entities**: +1. **Agent**: ID, role (extensible), platform, environment, capabilities, status, timestamp +2. **Message**: Protocol version, type, sender, recipient, task ID, priority, timestamp, correlation ID, payload +3. **Task**: ID, description, assigned agents, status, dependencies, GitHub reference +4. **Channel**: ID, type (Discord/SignalR/Redis/Relay), participants, config, security settings +5. **Capability**: Agent ID, role, platform, tools, languages, resource limits, metadata +6. **Configuration**: Channel settings, retention policy, token, webhook URLs, polling interval + +**Relationships**: +- Agent 1:N Messages (sent) +- Agent N:M Tasks (assignments) +- Channel 1:N Agents (participants) +- Channel 1:N Messages (history) +- Task N:M Tasks (dependencies) + +**State Transitions**: +- Agent: disconnected β†’ connecting β†’ connected β†’ disconnected +- Task: available β†’ assigned β†’ started β†’ (blocked|in_progress) β†’ (completed|failed) +- Message: queued β†’ sending β†’ sent β†’ delivered β†’ (acknowledged|failed) + +### API Contracts (contracts/) + +Generate the following contract files: + +1. **message-protocol.json**: JSON Schema for message format + ```json + { + "type": "object", + "required": ["protocolVersion", "messageType", "senderId", "timestamp"], + "properties": { + "protocolVersion": { "type": "string", "pattern": "^\\d+\\.\\d+$" }, + "messageType": { "enum": ["task_assigned", "task_started", ...] }, + "senderId": { "type": "string" }, + "recipientId": { "type": "string" }, + "taskId": { "type": "string" }, + "priority": { "type": "integer", "minimum": 0, "maximum": 10 }, + "timestamp": { "type": "string", "format": "date-time" }, + "correlationId": { "type": "string", "format": "uuid" }, + "payload": { "type": "object" } + } + } + ``` + +2. **capability-schema.json**: Agent capability registration format +3. **mcp-commands.yaml**: MCP command specifications (configure, join, status, etc.) +4. **relay-api.openapi.yaml**: Relay Server REST API specification (if implementing) + +### Quickstart Guide (quickstart.md) + +Create getting started guide covering: +- Installation (Docker vs npm) +- Configuration (channel setup, GitHub token, etc.) +- First agent connection +- Sample workflows + +### Agent Context Update + +Run `.specify/scripts/powershell/update-agent-context.ps1 -AgentType claude` to update agent-specific context with technologies from this plan. + +--- + +## Phase 2: Task Decomposition + +**Not created by `/speckit.plan`**. Use `/speckit.tasks` command after Phase 1 completion to generate `tasks.md`. + +--- + +## Next Steps + +1. βœ… Complete specification (`/speckit.specify` - DONE) +2. βœ… Clarify ambiguities (`/speckit.clarify` - DONE) +3. πŸ”„ **Current**: Generate implementation plan (`/speckit.plan` - IN PROGRESS) +4. ⏭️ Execute Phase 0 research (research agents) +5. ⏭️ Execute Phase 1 design (data-model.md, contracts/, quickstart.md) +6. ⏭️ Generate tasks (`/speckit.tasks`) +7. ⏭️ Begin implementation (`/speckit.implement`) + +**Status**: Plan structure complete. Proceeding to Phase 0 research... diff --git a/specs/001-multi-agent-coordination/quickstart.md b/specs/001-multi-agent-coordination/quickstart.md new file mode 100644 index 0000000..31c300b --- /dev/null +++ b/specs/001-multi-agent-coordination/quickstart.md @@ -0,0 +1,455 @@ +# Quick Start Guide: Multi-Agent Coordination System + +**Version**: 1.0 +**Last Updated**: 2026-02-14 + +## Overview + +This guide walks you through installing and configuring the CoorChat Multi-Agent Coordination System, connecting your first agent, and running a sample coordination workflow. + +--- + +## Prerequisites + +- **Node.js** v18+ (for MCP Server) +- **Docker** (recommended) OR npm +- **GitHub Personal Access Token** with `repo` scope +- **Channel Access**: One of the following: + - Discord Bot Token (for Discord channel) + - SignalR Hub URL (for SignalR channel) + - Redis Instance (for Redis channel) + - CoorChat Relay Server URL (for Relay channel) + +--- + +## Installation + +### Option 1: Docker (Recommended) + +```bash +# Pull the latest MCP Server image +docker pull coorchat/mcp-server:latest + +# Verify installation +docker run coorchat/mcp-server:latest --version +``` + +### Option 2: npm + +```bash +# Install globally +npm install -g coorchat + +# Verify installation +coorchat --version +``` + +--- + +## Configuration + +### Step 1: Initialize Configuration + +```bash +# Interactive configuration wizard +coorchat configure +``` + +The wizard will guide you through: + +1. **Channel Type Selection** + ``` + Select channel type: + [1] Discord + [2] SignalR + [3] Redis + [4] CoorChat Relay Server + + Choice: _ + ``` + +2. **Channel Connection Details** (example for Discord) + ``` + Enter channel connection details: + + Discord Bot Token: ${DISCORD_BOT_TOKEN} + Discord Guild ID: 123456789012345678 + Discord Channel ID: 987654321098765432 + ``` + +3. **GitHub Integration** + ``` + Enter GitHub repository details: + + GitHub Personal Access Token: ${GITHUB_TOKEN} + Repository URL: https://github.com/yourorg/yourrepo + Webhook Secret (optional): ${GITHUB_WEBHOOK_SECRET} + ``` + +4. **Retention & Logging** + ``` + Configure retention and logging: + + Message retention (days): 30 + Log level (ERROR/WARN/INFO/DEBUG): INFO + ``` + +### Step 2: Verify Configuration + +Configuration is saved to `.coorchat/config.json`: + +```json +{ + "version": "1.0", + "channel": { + "type": "discord", + "config": { + "guildId": "123456789012345678", + "channelId": "987654321098765432", + "botToken": "${DISCORD_BOT_TOKEN}" + }, + "retentionDays": 30, + "token": "${CHANNEL_TOKEN}" + }, + "github": { + "token": "${GITHUB_TOKEN}", + "repositoryUrl": "https://github.com/yourorg/yourrepo", + "webhookSecret": "${GITHUB_WEBHOOK_SECRET}", + "pollingIntervalSeconds": 30 + }, + "logging": { + "level": "INFO", + "outputs": ["console"], + "filePath": null + } +} +``` + +### Step 3: Set Environment Variables + +```bash +# Create .env file (NEVER commit this to git) +cat > .env <; + disconnect(): Promise; + sendMessage(message: Message): Promise; + onMessage(handler: (message: Message) => void): void; +} + +class ChannelFactory { + create(type: ChannelType, config: ChannelConfig): Channel { + switch (type) { + case 'discord': return new DiscordChannel(config); + case 'signalr': return new SignalRChannel(config); + case 'redis': return new RedisChannel(config); + case 'relay': return new RelayChannel(config); + } + } +} +``` + +### Rationale +- **Strategy Pattern**: Allows runtime channel switching without modifying client code +- **Factory Pattern**: Centralizes channel creation logic, simplifies testing with mock channels +- **Interface-based**: TypeScript interfaces ensure compile-time type safety across all channels +- **Testability**: Easy to mock channels for unit testing + +### Alternatives Considered +- **Plugin Architecture**: More flexible but adds complexity (dynamic loading, version management) + - Rejected: Over-engineered for 4 known channel types +- **Abstract Base Class**: More coupling than interface + - Rejected: Interface composition is more flexible in TypeScript + +--- + +## 2. Message Protocol Design + +### Decision +**JSON Schema with Semantic Versioning** + +Use JSON Schema for message validation with semantic versioning (major.minor) in message headers. + +```json +{ + "protocolVersion": "1.0", + "messageType": "task_assigned", + // ... message fields +} +``` + +**Compatibility Rules**: +- **Minor version changes** (1.0 β†’ 1.1): Additive only (new optional fields) +- **Major version changes** (1.x β†’ 2.0): Breaking changes allowed +- **Backward compatibility**: Support N and N-1 major versions + +### Rationale +- **JSON Schema**: Industry standard, excellent tooling (validation, code generation) +- **Semantic Versioning**: Clear compatibility contract +- **Header versioning**: Enables protocol negotiation before parsing payload +- **Validation**: Catch protocol errors early, provide clear error messages + +### Alternatives Considered +- **Protobuf**: More efficient but adds build complexity + - Rejected: JSON is human-readable, easier debugging, native in Node.js +- **GraphQL subscriptions**: Over-engineered for point-to-point messages + - Rejected: Not designed for peer-to-peer agent communication +- **No versioning**: Simpler but breaks on protocol changes + - Rejected: System needs to support gradual agent upgrades + +--- + +## 3. Real-time Performance + +### Decision +**Use native client libraries with connection pooling** + +- **Discord.js v14**: ~50-100 concurrent connections per process, ~50MB memory per bot +- **@microsoft/signalr**: ~1000 concurrent connections, ~2MB per connection +- **ioredis**: ~10,000 concurrent connections, ~1MB per connection + +**Performance Strategy**: +- Connection pooling for Redis (reuse connections) +- Single bot instance for Discord (Discord API limitation) +- SignalR hub connections per agent + +### Rationale +- **Discord.js**: Mature library, handles rate limiting automatically +- **SignalR Client**: Official Microsoft library, excellent TypeScript support +- **ioredis**: High-performance Redis client with cluster support +- **Meets Requirements**: 20-50 agents well within limits of all channels + +### Research Sources +- Discord.js documentation: Gateway intents, rate limiting +- SignalR performance benchmarks: Connection limits, message throughput +- ioredis benchmarks: Pipeline performance, memory usage + +### Alternatives Considered +- **Custom WebSocket implementation**: More control but reinvents wheel + - Rejected: Existing libraries handle reconnection, rate limiting, protocol complexities +- **Lower-level libraries (ws, net)**: More complexity + - Rejected: Discord, SignalR have specific protocol requirements + +--- + +## 4. GitHub Integration + +### Decision +**Webhooks primary, polling fallback with conditional requests** + +```typescript +class GitHubSyncManager { + async initialize() { + if (await this.setupWebhook()) { + this.webhookMode = true; + } else { + this.startPolling(interval = 30s); + } + } + + async poll() { + // Use If-Modified-Since, ETags for efficient polling + const response = await octokit.issues.listForRepo({ + headers: { 'If-Modified-Since': lastSync } + }); + if (response.status === 304) return; // Not modified + // Process changes + } +} +``` + +**Webhook Delivery**: +- GitHub guarantees "at least once" delivery +- Implement idempotency (deduplicate events by delivery ID) +- 30-second timeout for webhook endpoint + +**Polling Fallback**: +- Default: 30 seconds interval +- Use conditional requests (If-Modified-Since, ETag) to reduce API quota usage +- Exponential backoff on rate limit errors + +### Rationale +- **Webhooks**: Near real-time updates (<5s), efficient +- **Polling Fallback**: Works in restricted environments (CI/CD with no inbound connections) +- **Conditional Requests**: 60x API quota savings (304 responses don't count against quota) +- **Idempotency**: Prevents duplicate task notifications from webhook retries + +### Research Sources +- GitHub webhooks documentation: Delivery guarantees, retry logic +- GitHub API rate limiting: Conditional requests, quota management +- Octokit.js best practices + +### Alternatives Considered +- **Polling only**: Simpler but wastes API quota and adds latency + - Rejected: Webhooks provide better user experience +- **Webhooks only**: Doesn't work in restricted environments + - Rejected: Must support CI/CD pipelines without inbound connectivity +- **GraphQL subscriptions**: GitHub doesn't support real-time subscriptions + - Not available from GitHub + +--- + +## 5. Cross-platform Docker + +### Decision +**Multi-platform builds using Docker Buildx with GitHub Actions matrix** + +```yaml +# .github/workflows/mcp-server-ci.yml +strategy: + matrix: + platform: [linux/amd64, linux/arm64] + +steps: + - uses: docker/setup-buildx-action@v2 + - uses: docker/build-push-action@v4 + with: + platforms: ${{ matrix.platform }} + cache-from: type=gha + cache-to: type=gha,mode=max +``` + +**Image Optimization**: +- Multi-stage builds (build β†’ runtime) +- Alpine-based images for minimal size +- Layer caching with GitHub Actions cache +- Target size: <200MB (Node.js base ~50MB + deps ~100MB + app ~50MB) + +### Rationale +- **Buildx**: Official Docker tool for multi-platform builds +- **GitHub Actions**: Free for open source, integrated caching +- **Alpine**: Minimal base image (5MB vs Ubuntu 28MB) +- **Layer Caching**: Speeds up CI/CD, reduces build times from 10min to <2min + +### Research Sources +- Docker Buildx documentation +- GitHub Actions Docker build optimization guides +- Node.js Docker best practices (official Node.js Docker images) + +### Alternatives Considered +- **Build on native runners**: Slower, requires multiple runners + - Rejected: Buildx emulation faster than native ARM runners +- **Separate Dockerfiles per platform**: Maintenance burden + - Rejected: Single Dockerfile with buildx is cleaner + +--- + +## 6. Rate Limiting + +### Decision +**Exponential backoff with jitter and circuit breaker** + +```typescript +class ExponentialBackoff { + async retry(fn: () => Promise, maxRetries = 5): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + if (!isRetryable(error)) throw error; + const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s + const jitter = Math.random() * 1000; // 0-1s jitter + await sleep(baseDelay + jitter); + } + } + throw new Error('Max retries exceeded'); + } +} + +class CircuitBreaker { + // Open circuit after 5 consecutive failures + // Half-open after 60s, allow one request + // Close if request succeeds +} +``` + +**Rate Limit Header Parsing**: +- Discord: `X-RateLimit-Remaining`, `X-RateLimit-Reset` +- GitHub: `X-RateLimit-Remaining`, `X-RateLimit-Reset` +- Preemptive throttling when remaining < 10% + +### Rationale +- **Exponential Backoff**: Industry standard (2^n formula) +- **Jitter**: Prevents thundering herd (multiple agents retrying simultaneously) +- **Circuit Breaker**: Prevents cascading failures, fast-fails when service is down +- **Header Parsing**: Proactive rate limiting prevents 429 errors + +### Research Sources +- AWS Architecture Blog: Exponential Backoff and Jitter +- Discord API rate limiting documentation +- GitHub API rate limiting best practices +- Martin Fowler: Circuit Breaker pattern + +### Alternatives Considered +- **Linear backoff**: Less effective, longer retry times + - Rejected: Exponential is standard for distributed systems +- **No jitter**: Can cause thundering herd + - Rejected: Jitter is essential for multi-agent scenarios +- **Fixed delays**: Doesn't adapt to temporary vs persistent failures + - Rejected: Exponential backoff adapts better + +--- + +## 7. Configuration Management + +### Decision +**Zod for schema validation with environment variable substitution** + +```typescript +import { z } from 'zod'; + +const ConfigSchema = z.object({ + channel: z.object({ + type: z.enum(['discord', 'signalr', 'redis', 'relay']), + token: z.string().min(1), + retentionDays: z.number().int().positive().default(30), + }), + github: z.object({ + token: z.string().min(1), + webhookSecret: z.string().optional(), + }), +}); + +class ConfigLoader { + load(path: string): Config { + const raw = yaml.load(fs.readFileSync(path)); + const resolved = this.resolveEnvVars(raw); // ${GITHUB_TOKEN} β†’ process.env.GITHUB_TOKEN + return ConfigSchema.parse(resolved); // Validates and type-checks + } +} +``` + +**Security**: +- Secrets via environment variables, not committed to config files +- Config files in `.gitignore` (template in `.coorchat/config.template.yaml`) +- Environment variable substitution: `${VAR_NAME}` syntax + +### Rationale +- **Zod**: TypeScript-first validation, excellent error messages, type inference +- **YAML**: Human-readable, supports comments (better than JSON) +- **Env Var Substitution**: Follows 12-factor app principles, works in Docker +- **Schema Validation**: Catches configuration errors at startup, not runtime + +### Research Sources +- Zod documentation +- 12-Factor App: Config management +- Node.js dotenv best practices + +### Alternatives Considered +- **Joi**: Mature but not TypeScript-native + - Rejected: Zod has better TypeScript integration +- **JSON Schema**: More verbose, requires separate type definitions + - Rejected: Zod schemas are TypeScript types +- **Cosmiconfig**: Searches multiple config locations + - Rejected: Over-engineered for single config file + +--- + +## 8. Relay Server Storage + +### Decision +**Entity Framework Core with PostgreSQL and message retention policies** + +```csharp +public class RelayDbContext : DbContext { + public DbSet Channels { get; set; } + public DbSet Messages { get; set; } + public DbSet Agents { get; set; } +} + +// Message retention job +public class MessageRetentionService : BackgroundService { + protected override async Task ExecuteAsync(CancellationToken ct) { + while (!ct.IsCancellationRequested) { + await PurgeOldMessages(); + await Task.Delay(TimeSpan.FromHours(24), ct); + } + } + + private async Task PurgeOldMessages() { + var cutoff = DateTime.UtcNow.AddDays(-retentionDays); + await _context.Messages.Where(m => m.Timestamp < cutoff).ExecuteDeleteAsync(); + } +} +``` + +**Performance Optimizations**: +- Index on `Messages.Timestamp` for purge queries +- Index on `Messages.ChannelId` for history retrieval +- Connection pooling (default pool size: 100) +- Asynchronous I/O for all database operations + +### Rationale +- **Entity Framework Core**: Type-safe, migrations, excellent .NET integration +- **PostgreSQL**: Open source, JSON support (for message payloads), excellent performance +- **Retention Policies**: Automated cleanup prevents unbounded storage growth +- **Background Service**: .NET hosted service for scheduled tasks + +### Research Sources +- Entity Framework Core documentation: Performance best practices +- PostgreSQL performance tuning guides +- ASP.NET Core background services + +### Alternatives Considered +- **SQL Server**: Good performance but licensing costs + - Considered: Support both PostgreSQL and SQL Server (EF Core abstracts) +- **MongoDB**: Better for unstructured data but overkill for structured messages + - Rejected: Relational model fits message/channel/agent relationships +- **Dapper (micro-ORM)**: Faster but more manual SQL + - Rejected: EF Core performance is sufficient, type safety is valuable + +--- + +## Summary Table + +| Area | Decision | Key Rationale | +|------|----------|---------------| +| Channel Abstraction | Strategy + Factory patterns | Runtime flexibility, testability | +| Message Protocol | JSON Schema + Semantic Versioning | Standard tooling, clear compatibility | +| Real-time Libs | Discord.js, SignalR client, ioredis | Mature, performant, handles edge cases | +| GitHub Integration | Webhooks + Polling fallback | Real-time + universal compatibility | +| Docker Builds | Multi-platform Buildx + Alpine | Cross-platform, minimal size | +| Rate Limiting | Exponential backoff + Jitter + Circuit breaker | Prevents cascading failures | +| Config Management | Zod + YAML + Env vars | Type-safe, 12-factor compliance | +| Relay Storage | EF Core + PostgreSQL | Type-safe, migrations, performance | + +--- + +## Technology Stack Summary + +### MCP Server (TypeScript/Node.js) +- **Runtime**: Node.js 18+ (LTS) +- **Language**: TypeScript 5.x +- **Channel Clients**: Discord.js v14, @microsoft/signalr, ioredis +- **GitHub API**: @octokit/rest +- **Config**: yaml, zod +- **Testing**: Jest/Vitest, Playwright +- **Build**: esbuild (fast TypeScript compilation) + +### Relay Server (C#/.NET) +- **Runtime**: .NET 8.0 +- **Framework**: ASP.NET Core +- **ORM**: Entity Framework Core 8.0 +- **Database**: PostgreSQL 15+ (primary), SQL Server 2019+ (supported) +- **Real-time**: SignalR +- **Testing**: xUnit, TestContainers + +### Infrastructure +- **CI/CD**: GitHub Actions +- **Containers**: Docker with Buildx +- **Registry**: DockerHub or GitHub Container Registry +- **Package**: npm (for MCP Server) + +--- + +## Phase 0 Complete + +All technology decisions validated. No "NEEDS CLARIFICATION" items remaining. + +**Next**: Proceed to Phase 1 (Design & Contracts) diff --git a/specs/001-multi-agent-coordination/spec.md b/specs/001-multi-agent-coordination/spec.md new file mode 100644 index 0000000..a7e6a24 --- /dev/null +++ b/specs/001-multi-agent-coordination/spec.md @@ -0,0 +1,274 @@ +# Feature Specification: Multi-Agent Coordination System + +**Feature Branch**: `001-multi-agent-coordination` +**Created**: 2026-02-14 +**Status**: Draft +**Input**: User description: "we are building a coordination system between multiple agents (developer, tester, architect, frontend, backend, infrastructure etc) who are working together on a shared task. that channel is likely to be a realtime chat discord, signalr and similar or shared redis cache. this is a coordination channel where as many of the work items specification etc will be in a shared github repository. the channel needs to be secure between agents, simple to configure from the claude mcp command, easy to install and allow of a human to listen in to understand which agent is working on either directly in the channel i.e. discord or by asking an agent" + +## Clarifications + +### Session 2026-02-14 + +- Q: How should agents authenticate when joining the coordination channel? β†’ A: Shared token, self-generated +- Q: How long should conversation history be retained? β†’ A: Configurable on channel initiation +- Q: How should the system resolve conflicts when two agents try to claim the same task simultaneously? β†’ A: First-come-first-served (earliest timestamp wins) +- Q: What logging and monitoring should the system provide for debugging and operational visibility? β†’ A: Structured logging with configurable levels (ERROR, WARN, INFO, DEBUG) +- Q: How should the system handle protocol changes when agents with different protocol versions connect? β†’ A: Version in message header, backward compatible for 1 major version +- Q: How should the system be distributed and installed? β†’ A: Docker container via DockerHub, built with GitHub Actions +- Q: What installation method should users have? β†’ A: Single command Docker run or package manager install +- Q: What should the system be implemented in? β†’ A: TypeScript/Node.js +- Q: How should the system detect when tasks are added or updated in GitHub? β†’ A: Webhooks with polling fallback +- Q: Where should channel configuration be stored? β†’ A: Local file-based config (JSON/YAML) +- Q: Where should message history be stored? β†’ A: Channel provider's native storage (Discord/Redis/SignalR) +- Q: How should the system handle external API rate limit errors? β†’ A: Queue requests with exponential backoff retry +- Q: Should the system include a custom relay server as an alternative to third-party channels? β†’ A: Yes, C#/.NET relay server providing auth, message history, and config management + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Agent Task Coordination (Priority: P1) + +Multiple specialized agents (developer, tester, architect, frontend, backend, infrastructure) need to coordinate work on a shared software development task in real-time. + +**Why this priority**: This is the core value proposition - enabling agents to work together effectively. Without this, the system has no purpose. + +**Independent Test**: Can be fully tested by having two agents exchange task assignments and status updates through the coordination channel and delivers immediate visibility into distributed agent work. + +**Acceptance Scenarios**: + +1. **Given** multiple agents are connected to the coordination channel, **When** a new task is added to the GitHub repository, **Then** all relevant agents receive notification of the task +2. **Given** a developer agent completes a coding task, **When** the agent posts a status update, **Then** the tester agent receives the notification and can begin testing +3. **Given** agents are working on dependent tasks, **When** one agent updates their progress, **Then** dependent agents are notified of the change + +--- + +### User Story 2 - Human Observer Monitoring (Priority: P2) + +Project managers and team leads need to observe agent activities to understand project progress without disrupting agent workflows. + +**Why this priority**: Critical for trust and oversight, but agents can function without human observation. Enables humans to validate agent work and intervene when needed. + +**Independent Test**: Can be tested by having a human observer join the channel and query current agent activities, delivering visibility without active management involvement. + +**Acceptance Scenarios**: + +1. **Given** agents are actively working, **When** a human observer joins the coordination channel, **Then** they can see the conversation history showing which agents are working on what tasks +2. **Given** a human needs status information, **When** they ask any agent about current work, **Then** the agent responds with its current task and progress +3. **Given** multiple agents are working simultaneously, **When** a human views the channel, **Then** they can distinguish which messages came from which agent role (developer, tester, etc.) + +--- + +### User Story 3 - System Configuration and Setup (Priority: P3) + +System administrators need to quickly install and configure the coordination system for their team without extensive technical knowledge. + +**Why this priority**: Necessary for adoption but doesn't provide direct value until P1 functionality works. Enables teams to get started quickly. + +**Independent Test**: Can be tested by a new user completing full installation and configuration within expected timeframe, delivering a ready-to-use system. + +**Acceptance Scenarios**: + +1. **Given** a new installation, **When** administrator runs the installation command, **Then** all required dependencies are installed and the system is ready to use +2. **Given** the system is installed, **When** administrator runs the MCP configuration command, **Then** agents can be configured with their roles and connection details +3. **Given** configuration is complete, **When** the first agent connects, **Then** the coordination channel is automatically created and ready for use + +--- + +### User Story 4 - Secure Agent Communication (Priority: P1) + +Agents need to communicate securely to protect proprietary code, sensitive project information, and prevent unauthorized access. + +**Why this priority**: Security is non-negotiable for production use. Without secure communication, the system cannot be used for real projects. + +**Independent Test**: Can be tested by attempting unauthorized access to the channel and verifying it's blocked, while authorized agents can communicate freely. + +**Acceptance Scenarios**: + +1. **Given** agents are communicating in a channel, **When** an unauthorized party attempts to join, **Then** access is denied and no message content is visible +2. **Given** an agent needs to authenticate, **When** the agent connects using valid credentials, **Then** the agent is granted access to the coordination channel +3. **Given** messages are being transmitted, **When** monitoring network traffic, **Then** message content is encrypted and unreadable + +--- + +### User Story 5 - Agent Onboarding and Self-Management (Priority: P2) + +AI agents need to autonomously join the coordination system, register their capabilities, and manage their own lifecycle without human intervention. + +**Why this priority**: Essential for autonomous agent operation, but agents can be manually configured initially. Enables true multi-agent systems where agents self-organize. + +**Independent Test**: Can be tested by launching a new agent that automatically joins the channel, registers its capabilities, and begins receiving task assignments without manual setup. + +**Acceptance Scenarios**: + +1. **Given** a new agent is started with coordination credentials, **When** the agent initializes, **Then** it automatically connects to the channel and registers its role, platform, and capabilities +2. **Given** an agent reconnects after disconnection, **When** it rejoins the channel, **Then** it receives context about current tasks, other agents, and its previous state +3. **Given** multiple agents are connected, **When** an agent queries available capabilities, **Then** it receives a list of all connected agents with their roles, platforms, and capability sets +4. **Given** an agent completes a task, **When** it reports completion in the standard format, **Then** dependent agents are notified and task status is updated in GitHub + +--- + +### Edge Cases + +- What happens when an agent disconnects unexpectedly during a critical task update? +- How does the system handle message delivery when the coordination channel is temporarily unavailable? +- What happens when two agents try to claim the same task simultaneously? +- How does the system handle agents that send malformed messages or invalid status updates? +- What happens when the GitHub repository is unavailable but agents need to coordinate? +- How does the system behave when the maximum number of concurrent agents is reached? +- What happens when a CI/CD pipeline agent has firewall restrictions that block certain channel types? +- How does the system handle platform-specific failures (e.g., Windows path issues, Linux permission errors)? +- What happens when an agent's capabilities change mid-execution (e.g., loses network access, runs out of API quota)? +- How does the system handle agents running in different time zones or with clock skew? +- What happens when a task requires capabilities that no currently connected agent possesses? +- What happens when two agents register with the same custom role name but different capability sets? +- How does the system handle malformed capability registration data? +- What happens when an agent sends messages in an incorrect or outdated protocol format? +- How does the system handle very large capability sets that might exceed message size limits? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST support extensible agent roles, allowing users to define custom agent types on the fly in addition to common roles like developer, tester, architect, frontend, backend, and infrastructure +- **FR-002**: System MUST provide real-time message exchange between agents with delivery confirmation +- **FR-003**: System MUST integrate with GitHub repositories to sync work items, specifications, and task status using GitHub webhooks as the primary mechanism with polling fallback for environments where webhooks cannot be configured +- **FR-003a**: System MUST support configurable polling interval (default 30 seconds) when operating in polling mode +- **FR-004**: System MUST support four communication channel types (Discord, SignalR, Redis cache, and custom CoorChat Relay Server) through a pluggable channel architecture that allows teams to choose their preferred channel +- **FR-005**: System MUST authenticate and authorize agents before granting channel access using a shared token per coordination channel that is self-generated during channel setup +- **FR-005a**: System MUST provide a mechanism to generate, display, and rotate the shared channel authentication token via MCP commands +- **FR-006**: System MUST encrypt all agent-to-agent communications +- **FR-007**: System MUST allow human observers to view agent communications without participating +- **FR-008**: System MUST provide a configuration interface accessible via Claude MCP commands +- **FR-009**: System MUST include automated installation process requiring minimal manual setup +- **FR-010**: System MUST identify each agent by its role (developer, tester, etc.) in all communications +- **FR-011**: System MUST persist conversation history for later review using the channel provider's native storage mechanism (Discord message history, Redis persistence, SignalR backing store) with a configurable retention period set during channel initialization +- **FR-011a**: System MUST automatically purge conversation history older than the configured retention period to manage storage, using channel-specific cleanup mechanisms +- **FR-012**: System MUST allow agents to query the status of other agents +- **FR-013**: System MUST notify relevant agents when dependent tasks change status +- **FR-014**: System MUST handle agent disconnections gracefully and allow reconnection +- **FR-015**: System MUST provide message ordering guarantees so agents see consistent task state +- **FR-015a**: System MUST resolve concurrent task claiming conflicts using first-come-first-served based on message timestamps, with the earliest timestamp winning the task assignment +- **FR-016**: System MUST support agents running on multiple operating systems including Linux, macOS, and Windows +- **FR-017**: System MUST support agents running in CI/CD pipeline environments including GitHub Actions, Azure DevOps, and AWS services +- **FR-018**: Each agent MUST be able to advertise its platform, environment, and capabilities to other agents +- **FR-019**: Agents MUST be able to discover and query the capabilities of other connected agents +- **FR-020**: System MUST handle platform-specific limitations (e.g., firewall restrictions in CI/CD pipelines, proxy requirements) +- **FR-021**: System MUST provide platform-agnostic client libraries or connection methods for all supported channels +- **FR-022**: System MUST provide a plugin architecture that allows future extension with custom communication channels, agent capabilities, or integration points +- **FR-023**: System MUST use a structured message protocol (such as JSON) with standardized fields including protocol version, message type, sender ID, task ID, priority, timestamp, and content +- **FR-023a**: System MUST maintain backward compatibility for one major protocol version, allowing agents with different protocol versions to communicate during upgrade transitions +- **FR-024**: System MUST provide an agent capability registration protocol that allows agents to declare their role, platform, environment, available tools, supported languages, and API access +- **FR-025**: System MUST define standardized task lifecycle events including task assigned, task started, task blocked, task progress update, task completed, and task failed +- **FR-026**: System MUST provide MCP command interface with simple text commands and visual feedback using text-based graphics for status display +- **FR-027**: System MUST allow users to define and register new custom agent role types at runtime without code changes +- **FR-028**: System MUST provide structured logging with configurable log levels (ERROR, WARN, INFO, DEBUG) for debugging and operational visibility +- **FR-028a**: System MUST log key events including agent connections/disconnections, task assignments, message delivery failures, authentication failures, and channel errors +- **FR-029**: System MUST be implemented in TypeScript/Node.js and packaged as a Docker container via DockerHub (or GitHub Container Registry) +- **FR-029a**: Docker container MUST support all three communication channel types (Discord, SignalR, Redis) and allow channel selection via environment variables +- **FR-030**: System MUST use GitHub Actions for automated build, test, and release pipelines +- **FR-030a**: Build pipeline MUST produce multi-platform Docker images supporting linux/amd64, linux/arm64, and Windows containers +- **FR-031**: System MUST support single-command installation via Docker run with environment variable configuration +- **FR-031a**: System MUST provide installation via npm package manager as an alternative to Docker for Node.js environments +- **FR-032**: System MUST store channel configuration (tokens, retention periods, webhook URLs, polling intervals) in local JSON or YAML files (e.g., `.coorchat/config.json`) +- **FR-032a**: Configuration files MUST support environment variable substitution for sensitive values (e.g., `${GITHUB_TOKEN}`) +- **FR-033**: System MUST handle external API rate limits (Discord, GitHub, etc.) by queuing requests and retrying with exponential backoff +- **FR-033a**: System MUST respect rate limit headers from external APIs and automatically throttle requests to prevent exceeding limits +- **FR-034**: System MUST include a custom CoorChat Relay Server implemented in C#/.NET as an alternative to third-party channel providers +- **FR-034a**: CoorChat Relay Server MUST be deployable as a hosted Docker container +- **FR-034b**: CoorChat Relay Server MUST provide authenticated communications relay between agents using the shared token mechanism +- **FR-034c**: CoorChat Relay Server MUST provide centralized message history storage with configurable retention +- **FR-034d**: CoorChat Relay Server MUST provide centralized configuration management for agent channels +- **FR-034e**: CoorChat Relay Server MUST be designed to integrate seamlessly with the MCP server component +- **FR-034f**: CoorChat Relay Server MUST support the same message protocol (JSON with versioning) as other channel types + +### Key Entities + +- **Agent**: Represents a specialized AI agent with a user-defined or standard role type (e.g., developer, tester, architect, security-auditor, documentation-writer, custom roles). Key attributes include agent ID, role type (extensible/customizable), connection status, current task assignment, authentication credentials, operating system (Linux/macOS/Windows), execution environment (local machine, GitHub Actions, Azure DevOps, AWS), capability set (available tools, supported languages, API access, permissions, resource constraints), and registration timestamp. +- **Coordination Channel**: Represents the real-time communication space where agents exchange messages. Key attributes include channel ID, participant list, message history, security settings. +- **Task**: Represents a work item from the GitHub repository. Key attributes include task ID, description, assigned agents, status, dependencies, GitHub issue/PR reference. +- **Message**: Represents a structured communication between agents following a standardized protocol. Key attributes include sender agent ID, recipient (specific agent or broadcast), timestamp, message type (task_assigned, task_started, task_blocked, task_progress, task_completed, task_failed, capability_query, status_query, response, error), task ID (if applicable), priority level, content payload, correlation ID (for request/response matching), and delivery status. +- **Agent Capability**: Represents the declared capabilities of an agent. Key attributes include agent ID, role type, platform (OS), environment type, tool list (available commands/APIs), language support, resource limits (API quotas, rate limits), and custom capability metadata. +- **Human Observer**: Represents a human user monitoring agent activity. Key attributes include user ID, permissions, observation mode (passive viewer vs interactive). +- **CoorChat Relay Server**: Represents the optional custom relay server component (C#/.NET implementation). Key attributes include server URL, supported protocol version, active channels, connected agents, message throughput, storage backend, authentication configuration. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Agents receive and acknowledge messages within 2 seconds under normal network conditions +- **SC-002**: System supports at least 20 concurrent agents without message delivery delays +- **SC-003**: System installation completes in under 5 minutes on a standard development machine +- **SC-004**: Configuration of a new agent (role, credentials, channel) takes under 2 minutes via MCP commands +- **SC-005**: Human observers can identify which agent is working on a specific task within 10 seconds of joining the channel +- **SC-006**: 100% of agent communications are encrypted end-to-end +- **SC-007**: System maintains 99.9% message delivery success rate (messages delivered within 30 seconds or retry) +- **SC-008**: Unauthorized access attempts are blocked with 100% success rate +- **SC-009**: Agents can resume work within 30 seconds after unexpected disconnection +- **SC-010**: GitHub work item synchronization completes within 5 seconds of repository updates +- **SC-011**: Agents successfully connect and communicate from all supported platforms (Linux, macOS, Windows) and environments (local, GitHub Actions, Azure DevOps, AWS) +- **SC-012**: Agents can discover capabilities of other agents within 5 seconds of connection +- **SC-013**: System supports at least one communication channel type that works in restricted CI/CD environments +- **SC-014**: Agents can autonomously join the channel and register capabilities without human intervention +- **SC-015**: Custom agent roles can be defined and registered in under 30 seconds +- **SC-016**: All inter-agent messages follow the structured protocol with 100% compliance +- **SC-017**: MCP commands provide visual feedback within 1 second of execution +- **SC-018**: Docker container installation completes in under 2 minutes (image pull + startup) +- **SC-019**: System builds successfully on all target platforms (Linux amd64/arm64, Windows) in under 10 minutes +- **SC-020**: CoorChat Relay Server supports at least 50 concurrent agent connections with sub-100ms message latency +- **SC-021**: CoorChat Relay Server deploys as Docker container in under 2 minutes + +## Assumptions + +- All agents have network access to reach the coordination channel infrastructure (though specific protocols may vary by environment) +- Docker is available on agent host machines (or package manager for non-Docker installation) +- Agent environments can pull Docker images from DockerHub or GitHub Container Registry +- GitHub repository is configured with appropriate access tokens for the system +- At least one of the three supported communication channels (Discord, SignalR, Redis) is accessible from each agent's environment +- Human observers have appropriate permissions to access the coordination channel +- The system operates in an environment where encrypted communication is permitted +- Agents are developed using Claude API or compatible AI agent frameworks +- Team size is typically 20 or fewer concurrent agents (scalability beyond this is not prioritized) +- Communication channel infrastructure (Discord server, SignalR hub, or Redis instance) is provided separately or easily provisioned +- Agents running in CI/CD pipelines have sufficient permissions and network access for their required operations +- The pluggable channel architecture allows for future extensibility and custom channel implementations +- CI/CD environments (GitHub Actions, Azure DevOps, AWS) support Docker container execution + +## Dependencies + +### MCP Server Component (TypeScript/Node.js) +- Node.js runtime (v18 or later) for npm-based installation +- TypeScript compiler and Node.js ecosystem for development +- GitHub repository for source code hosting and work item synchronization via GitHub API +- GitHub Actions for CI/CD pipeline execution +- DockerHub or GitHub Container Registry for container image hosting +- Docker runtime on agent host machines for container-based installation +- Claude MCP server capability for configuration commands + +### CoorChat Relay Server Component (C#/.NET) +- .NET 8.0 runtime or later +- C# compiler and .NET ecosystem for development +- Docker runtime for container deployment +- GitHub Actions for CI/CD pipeline execution + +### External Services (Optional, based on channel choice) +- Discord API (if using Discord channel) +- SignalR server (if using SignalR channel) +- Redis instance (if using Redis channel) +- CoorChat Relay Server (if using custom relay channel) + +### Common Dependencies +- Authentication service for agent and human user verification (built into Relay Server or external) +- Network connectivity between all participating agents and the chosen channel + +## Out of Scope + +- Building custom AI agents (assumes agents exist and need coordination) +- Project management features beyond basic task status tracking +- Code repository hosting (uses existing GitHub) +- Video or voice communication between agents or humans +- Advanced analytics or reporting on agent performance +- Integration with project management tools beyond GitHub (Jira, Asana, etc.) in the initial release +- Multi-team or multi-project coordination in a single channel +- Custom agent behavior or task execution logic +- Development of custom plugins beyond the four core channels (initial release focuses on Discord, SignalR, Redis, and CoorChat Relay Server) +- GUI-based configuration tools (initial release uses MCP commands, GUI could be a future plugin) +- Detailed implementation specification for CoorChat Relay Server (this spec defines interface and integration requirements; relay server implementation details are a separate specification) diff --git a/specs/001-multi-agent-coordination/tasks.md b/specs/001-multi-agent-coordination/tasks.md new file mode 100644 index 0000000..dbadd98 --- /dev/null +++ b/specs/001-multi-agent-coordination/tasks.md @@ -0,0 +1,489 @@ +# Implementation Tasks: Multi-Agent Coordination System + +**Feature**: 001-multi-agent-coordination +**Branch**: `001-multi-agent-coordination` +**Date**: 2026-02-14 +**Generated by**: `/speckit.tasks` + +## Overview + +This document provides a complete, dependency-ordered task breakdown for implementing the Multi-Agent Coordination System. Tasks are organized by user story to enable independent implementation and testing of each feature increment. + +**Total Tasks**: 87 +**MVP Scope**: Phase 3 (User Story 1) + Phase 4 (User Story 4) = **25 tasks** +**Parallelization Opportunities**: 42 tasks marked [P] can run in parallel + +--- + +## Implementation Strategy + +### MVP-First Approach + +The recommended implementation order follows the Priority P1 user stories: + +1. **Phase 1-2**: Setup and Foundation (9 tasks) - Blocking prerequisites +2. **Phase 3**: User Story 1 - Agent Task Coordination (P1) - Core functionality +3. **Phase 4**: User Story 4 - Secure Agent Communication (P1) - Security essentials +4. **Phase 5+**: Remaining user stories (P2, P3) - Feature completeness + +**MVP Delivery**: Phases 1-4 provide a fully functional, secure agent coordination system. + +### Independent Story Testing + +Each user story phase includes: +- **Story Goal**: What value this story delivers +- **Independent Test**: How to validate the story works standalone +- **Implementation Tasks**: All code needed for this story +- **No external dependencies**: Can be implemented without waiting for other stories (except foundational phase) + +--- + +## Dependency Graph + +``` +Phase 1 (Setup) + ↓ +Phase 2 (Foundation) + ↓ + β”œβ”€β†’ Phase 3: User Story 1 (P1) - Agent Task Coordination + β”‚ ↓ + β”œβ”€β†’ Phase 4: User Story 4 (P1) - Secure Communication + β”‚ ↓ + β”œβ”€β†’ Phase 5: User Story 5 (P2) - Agent Onboarding + β”‚ ↓ + β”œβ”€β†’ Phase 6: User Story 2 (P2) - Human Observer + β”‚ ↓ + └─→ Phase 7: User Story 3 (P3) - Config & Setup + ↓ + Phase 8: Polish & Integration +``` + +**Key**: P1 stories (Phases 3-4) are MVP. P2/P3 stories (Phases 5-7) add features incrementally. + +--- + +## Phase 1: Project Setup + +**Goal**: Initialize project structure, dependencies, and development environment + +**Duration**: ~2-4 hours + +### Tasks + +- [x] T001 Create monorepo structure with packages/mcp-server/ and packages/relay-server/ +- [x] T002 [P] Initialize MCP Server (TypeScript): package.json with dependencies (discord.js, @microsoft/signalr, ioredis, @octokit/rest, zod, yaml) +- [x] T003 [P] Initialize Relay Server (C#/.NET): Create solution file packages/relay-server/CoorChat.RelayServer.sln +- [x] T004 [P] Setup TypeScript configuration in packages/mcp-server/tsconfig.json (target ES2022, module ESNext, strict mode) +- [x] T005 [P] Create Dockerfile for MCP Server in packages/mcp-server/Dockerfile (multi-stage build, Alpine base) +- [x] T006 [P] Create Dockerfile for Relay Server in packages/relay-server/Dockerfile +- [x] T007 Setup GitHub Actions workflow .github/workflows/mcp-server-ci.yml (build, test, publish to DockerHub) +- [x] T008 [P] Setup GitHub Actions workflow .github/workflows/relay-server-ci.yml +- [x] T009 Create docker-compose.yml for local development environment + +**Completion Criteria**: All project structures exist, dependencies installable, Docker builds succeed + +--- + +## Phase 2: Foundational Layer + +**Goal**: Implement core abstractions and protocols that all user stories depend on + +**Duration**: ~8-12 hours + +**Why Foundation First**: These components are used by ALL user stories, so they must be complete before any story implementation. + +### Protocol & Message Infrastructure + +- [x] T010 [P] Define Message interface and types in packages/mcp-server/src/protocol/Message.ts (protocolVersion, messageType enum, all fields from message-protocol.json) +- [x] T011 [P] Implement MessageBuilder fluent API in packages/mcp-server/src/protocol/MessageBuilder.ts +- [x] T012 [P] Implement MessageValidator using JSON schema in packages/mcp-server/src/protocol/MessageValidator.ts +- [x] T013 [P] Implement VersionManager for protocol versioning in packages/mcp-server/src/protocol/VersionManager.ts (backward compatibility for 1 major version) + +### Channel Abstraction Layer + +- [x] T014 Create Channel interface in packages/mcp-server/src/channels/base/Channel.ts (connect, disconnect, sendMessage, onMessage methods) +- [x] T015 [P] Implement ChannelFactory in packages/mcp-server/src/channels/base/ChannelFactory.ts (Strategy pattern, creates Discord/SignalR/Redis/Relay channels) +- [x] T016 [P] Create ChannelAdapter base class in packages/mcp-server/src/channels/base/ChannelAdapter.ts (common functionality: reconnection, error handling) + +### Core Entities + +- [x] T017 [P] Define Agent interface in packages/mcp-server/src/agents/Agent.ts (id, role, platform, environment, capabilities, status, timestamps) +- [x] T018 [P] Define Task interface in packages/mcp-server/src/tasks/Task.ts (id, description, assignedAgents, status, dependencies, GitHub references) +- [x] T019 [P] Define Capability interface in packages/mcp-server/src/agents/Capability.ts (per capability-schema.json) + +### Configuration Management + +- [x] T020 Create ConfigLoader in packages/mcp-server/src/config/ConfigLoader.ts (load JSON/YAML, environment variable substitution) +- [x] T021 [P] Create ConfigValidator using Zod in packages/mcp-server/src/config/ConfigValidator.ts (validate config schema from plan.md) +- [x] T022 [P] Create EnvironmentResolver in packages/mcp-server/src/config/EnvironmentResolver.ts (resolve ${VAR_NAME} syntax) + +### Logging Infrastructure + +- [x] T023 [P] Create Logger interface in packages/mcp-server/src/logging/Logger.ts (ERROR, WARN, INFO, DEBUG levels) +- [x] T024 [P] Implement LogFormatter in packages/mcp-server/src/logging/LogFormatter.ts (structured JSON logging) + +**Completion Criteria**: Protocol defined, channel abstraction complete, entities modeled, config system working, logging operational + +--- + +## Phase 3: User Story 1 - Agent Task Coordination (Priority P1) + +**Story Goal**: Enable multiple specialized agents to coordinate work on shared tasks in real-time via GitHub integration + +**Independent Test**: +1. Start two agents (developer and tester roles) +2. Create GitHub issue in connected repository +3. Verify developer agent receives task notification +4. Developer sends task_started message +5. Verify tester agent sees status update +6. Developer sends task_completed message +7. Verify tester agent receives completion notification + +**Test Command**: `npm run test:integration -- --grep="Agent Task Coordination"` + +### GitHub Integration + +- [x] T025 [P] [US1] Implement GitHubClient wrapper in packages/mcp-server/src/github/GitHubClient.ts (using @octokit/rest, auth with token) +- [x] T026 [P] [US1] Implement WebhookHandler in packages/mcp-server/src/github/WebhookHandler.ts (Express endpoint, signature validation, parse issue/PR events) +- [x] T027 [P] [US1] Implement PollingService in packages/mcp-server/src/github/PollingService.ts (fallback polling with 30s interval, conditional requests with ETags) +- [x] T028 [US1] Implement SyncManager in packages/mcp-server/src/github/SyncManager.ts (orchestrate webhook + polling, deduplicate events, map issues β†’ tasks) + +### Task Management + +- [x] T029 [P] [US1] Implement TaskQueue in packages/mcp-server/src/tasks/TaskQueue.ts (FIFO queue, task assignment logic) +- [x] T030 [P] [US1] Implement ConflictResolver in packages/mcp-server/src/tasks/ConflictResolver.ts (timestamp-based first-come-first-served) +- [x] T031 [P] [US1] Implement DependencyTracker in packages/mcp-server/src/tasks/DependencyTracker.ts (track task dependencies, notify when dependencies complete) + +### Agent Registry + +- [x] T032 [P] [US1] Implement AgentRegistry in packages/mcp-server/src/agents/AgentRegistry.ts (track connected agents, add/remove, get by ID/role) +- [x] T033 [P] [US1] Implement RoleManager in packages/mcp-server/src/agents/RoleManager.ts (extensible role definitions, validate custom roles) + +### Channel Implementations + +- [x] T034 [P] [US1] Implement DiscordChannel in packages/mcp-server/src/channels/discord/DiscordChannel.ts (Discord.js client, bot connection, send/receive messages) +- [x] T035 [P] [US1] Implement SignalRChannel in packages/mcp-server/src/channels/signalr/SignalRChannel.ts (@microsoft/signalr client, hub connection) +- [x] T036 [P] [US1] Implement RedisChannel in packages/mcp-server/src/channels/redis/RedisChannel.ts (ioredis pub/sub, message serialization) + +### Message Coordination + +- [x] T037 [US1] Implement message routing logic in packages/mcp-server/src/channels/base/ChannelAdapter.ts (broadcast vs unicast, task notifications) +- [x] T038 [US1] Implement task lifecycle event handlers in packages/mcp-server/src/tasks/TaskQueue.ts (task_assigned, task_started, task_completed, task_failed) + +### Integration Test + +- [x] T039 [US1] Create integration test packages/mcp-server/tests/integration/agent-task-coordination.test.ts (simulates GitHub issue β†’ task assignment β†’ agent coordination workflow) + +**Phase 3 Completion Criteria**: +- βœ… GitHub webhooks working (or polling fallback active) +- βœ… Tasks created from GitHub issues +- βœ… Agents receive task assignments +- βœ… Status updates broadcast to all agents +- βœ… Task completion triggers notifications +- βœ… Independent test passes + +--- + +## Phase 4: User Story 4 - Secure Agent Communication (Priority P1) + +**Story Goal**: Protect agent communications with authentication and encryption + +**Independent Test**: +1. Configure channel with shared token +2. Start agent with correct token β†’ connects successfully +3. Attempt connection with wrong token β†’ rejected +4. Monitor network traffic β†’ verify encryption (TLS) +5. Verify message content unreadable without token + +**Test Command**: `npm run test:integration -- --grep="Secure Communication"` + +### Authentication + +- [ ] T040 [P] [US4] Implement token-based authentication in packages/mcp-server/src/channels/base/ChannelAdapter.ts (validate shared token on connection) +- [ ] T041 [P] [US4] Implement TokenGenerator in packages/mcp-server/src/config/TokenGenerator.ts (generate secure random tokens) +- [ ] T042 [P] [US4] Add authentication middleware to Discord channel in packages/mcp-server/src/channels/discord/DiscordChannel.ts +- [ ] T043 [P] [US4] Add authentication middleware to SignalR channel in packages/mcp-server/src/channels/signalr/SignalRChannel.ts +- [ ] T044 [P] [US4] Add authentication middleware to Redis channel in packages/mcp-server/src/channels/redis/RedisChannel.ts + +### Encryption + +- [ ] T045 [P] [US4] Configure TLS for SignalR connections in packages/mcp-server/src/channels/signalr/SignalRChannel.ts +- [ ] T046 [P] [US4] Configure TLS for Redis connections in packages/mcp-server/src/channels/redis/RedisChannel.ts (--tls option) + +### Relay Server Authentication (Optional Component) + +- [ ] T047 [P] [US4] Implement AuthenticationMiddleware in packages/relay-server/src/CoorChat.RelayServer.Api/Middleware/AuthenticationMiddleware.cs +- [ ] T048 [P] [US4] Implement AuthenticationService in packages/relay-server/src/CoorChat.RelayServer.Core/Services/AuthenticationService.cs (validate shared token) + +### Security Testing + +- [ ] T049 [US4] Create security test packages/mcp-server/tests/integration/secure-communication.test.ts (test authentication rejection, verify encryption) + +**Phase 4 Completion Criteria**: +- βœ… Shared token authentication working +- βœ… Unauthorized access blocked +- βœ… TLS/encryption enabled for all channels +- βœ… Network traffic encrypted +- βœ… Independent test passes + +--- + +## Phase 5: User Story 5 - Agent Onboarding and Self-Management (Priority P2) + +**Story Goal**: Enable agents to autonomously join, register capabilities, and manage their lifecycle + +**Independent Test**: +1. Start new agent with credentials (no manual setup) +2. Agent automatically connects and registers +3. Query agent capabilities β†’ verify advertised correctly +4. Stop agent (disconnect) +5. Restart agent β†’ verify rejoins and restores state + +**Test Command**: `npm run test:integration -- --grep="Agent Onboarding"` + +### Capability Management + +- [ ] T050 [P] [US5] Implement CapabilityManager in packages/mcp-server/src/agents/CapabilityManager.ts (register, discover, query capabilities) +- [ ] T051 [P] [US5] Implement auto-detection of platform/environment in packages/mcp-server/src/agents/CapabilityDetector.ts (OS, environment type, installed tools) +- [ ] T052 [US5] Add capability_query and capability_response message handlers in packages/mcp-server/src/protocol/MessageHandler.ts + +### Lifecycle Management + +- [ ] T053 [P] [US5] Implement heartbeat mechanism in packages/mcp-server/src/agents/HeartbeatService.ts (15s interval, detect disconnections after 30s) +- [ ] T054 [P] [US5] Implement graceful shutdown handler in packages/mcp-server/src/agents/LifecycleManager.ts (wait for pending tasks, send agent_left message) +- [ ] T055 [P] [US5] Implement reconnection logic with state restoration in packages/mcp-server/src/channels/base/ChannelAdapter.ts (restore agent context, resume tasks) + +### Agent Context Persistence + +- [ ] T056 [P] [US5] Implement AgentStateStore in packages/mcp-server/src/agents/AgentStateStore.ts (save/load agent state to local file) + +### Integration Test + +- [ ] T057 [US5] Create integration test packages/mcp-server/tests/integration/agent-onboarding.test.ts (autonomous join, capability registration, reconnection) + +**Phase 5 Completion Criteria**: +- βœ… Agents self-register on startup +- βœ… Capabilities auto-detected and advertised +- βœ… Heartbeat keeps connections alive +- βœ… Graceful shutdown works +- βœ… Reconnection restores state +- βœ… Independent test passes + +--- + +## Phase 6: User Story 2 - Human Observer Monitoring (Priority P2) + +**Story Goal**: Allow humans to observe agent activities without disrupting workflows + +**Independent Test**: +1. Start multiple agents working on tasks +2. Human joins channel as observer +3. Query agent status β†’ verify response shows current tasks +4. View channel history β†’ verify all agent messages visible +5. Distinguish agent roles in messages + +**Test Command**: `npm run test:integration -- --grep="Human Observer"` + +### MCP Command Interface + +- [ ] T058 [P] [US2] Implement CommandHandler in packages/mcp-server/src/mcp/CommandHandler.ts (dispatch /coorchat commands) +- [ ] T059 [P] [US2] Implement StatusCommand in packages/mcp-server/src/mcp/commands/StatusCommand.ts (show channel status, connected agents, active tasks) +- [ ] T060 [P] [US2] Implement CapabilitiesCommand in packages/mcp-server/src/mcp/commands/CapabilitiesCommand.ts (list all agent capabilities) + +### Text-based UI + +- [ ] T061 [P] [US2] Implement TextUI for status display in packages/mcp-server/src/mcp/ui/TextUI.ts (ASCII tables, box drawing, agent icons) +- [ ] T062 [US2] Format agent status display in packages/mcp-server/src/mcp/ui/AgentStatusFormatter.ts (role, platform, current task, online/offline indicator) + +### Observer Mode + +- [ ] T063 [P] [US2] Add observer role to AgentRegistry in packages/mcp-server/src/agents/AgentRegistry.ts (passive viewer vs interactive participant) +- [ ] T064 [P] [US2] Implement message history retrieval in packages/mcp-server/src/channels/base/ChannelAdapter.ts (fetch recent messages from channel) + +### Integration Test + +- [ ] T065 [US2] Create integration test packages/mcp-server/tests/integration/human-observer.test.ts (join as observer, query status, view history) + +**Phase 6 Completion Criteria**: +- βœ… /coorchat status command works +- βœ… Text UI displays agent information clearly +- βœ… Observer can view message history +- βœ… Observer doesn't disrupt agent coordination +- βœ… Independent test passes + +--- + +## Phase 7: User Story 3 - System Configuration and Setup (Priority P3) + +**Story Goal**: Simplify installation and configuration for administrators + +**Independent Test**: +1. Fresh environment (no prior setup) +2. Run /coorchat configure +3. Complete interactive prompts +4. Verify config file created with correct settings +5. Run /coorchat join +6. Verify agent connects successfully + +**Test Command**: `npm run test:integration -- --grep="Configuration"` + +### MCP Configuration Commands + +- [ ] T066 [P] [US3] Implement ConfigureCommand in packages/mcp-server/src/mcp/commands/ConfigureCommand.ts (interactive wizard per mcp-commands.yaml) +- [ ] T067 [P] [US3] Implement JoinCommand in packages/mcp-server/src/mcp/commands/JoinCommand.ts (agent onboarding wizard) +- [ ] T068 [P] [US3] Implement DisconnectCommand in packages/mcp-server/src/mcp/commands/DisconnectCommand.ts (graceful shutdown) + +### Interactive Prompts + +- [ ] T069 [P] [US3] Implement PromptManager in packages/mcp-server/src/mcp/ui/PromptManager.ts (step-by-step prompts, input validation) +- [ ] T070 [P] [US3] Create channel type selection UI in packages/mcp-server/src/mcp/ui/ChannelSelector.ts (Discord/SignalR/Redis/Relay options) +- [ ] T071 [P] [US3] Create GitHub configuration prompts in packages/mcp-server/src/mcp/ui/GitHubConfigurator.ts (token, repo URL, webhook secret) + +### Installation Automation + +- [ ] T072 [P] [US3] Create npm post-install script in packages/mcp-server/package.json (initialize config template) +- [ ] T073 [P] [US3] Create config template file .coorchat/config.template.yaml (documented example configuration) + +### Integration Test + +- [ ] T074 [US3] Create integration test packages/mcp-server/tests/integration/configuration.test.ts (run configure command, validate generated config) + +**Phase 7 Completion Criteria**: +- βœ… /coorchat configure creates valid config +- βœ… Interactive prompts guide user through setup +- βœ… /coorchat join connects agent successfully +- βœ… Installation under 5 minutes +- βœ… Independent test passes + +--- + +## Phase 8: Polish & Cross-Cutting Concerns + +**Goal**: Production readiness, error handling, performance optimization + +### Error Handling & Retry Logic + +- [ ] T075 [P] Implement RetryQueue in packages/mcp-server/src/retry/RetryQueue.ts (queue failed messages for retry) +- [ ] T076 [P] Implement ExponentialBackoff in packages/mcp-server/src/retry/ExponentialBackoff.ts (2^n backoff with jitter) +- [ ] T077 [P] Implement RateLimiter in packages/mcp-server/src/retry/RateLimiter.ts (respect GitHub/Discord API rate limits) +- [ ] T078 [P] Add circuit breaker to channel connections in packages/mcp-server/src/channels/base/ChannelAdapter.ts (open after 5 failures, retry after 60s) + +### Message History & Retention + +- [ ] T079 [P] Implement history retention policies in packages/mcp-server/src/channels/base/ChannelAdapter.ts (purge messages older than configured retention) +- [ ] T080 [P] For Relay Server: Implement MessageRetentionService in packages/relay-server/src/CoorChat.RelayServer.Core/Services/MessageRetentionService.cs (background job, daily purge) + +### Performance Optimization + +- [ ] T081 [P] Add connection pooling for Redis in packages/mcp-server/src/channels/redis/RedisChannel.ts (reuse connections, lazy initialization) +- [ ] T082 [P] Implement message batching for high-throughput scenarios in packages/mcp-server/src/protocol/MessageBatcher.ts + +### Documentation + +- [ ] T083 [P] Create API documentation in packages/mcp-server/README.md (usage examples, API reference) +- [ ] T084 [P] Create deployment guide in docs/DEPLOYMENT.md (Docker, npm, environment variables) + +### Build & Distribution + +- [ ] T085 [P] Optimize Docker images in Dockerfiles (multi-stage build, layer caching, target <200MB) +- [ ] T086 Publish to npm registry (configure package.json, CI/CD pipeline) +- [ ] T087 Publish to DockerHub (configure GitHub Actions workflows, semantic versioning) + +**Phase 8 Completion Criteria**: +- βœ… Error handling robust across all channels +- βœ… Rate limiting prevents API quota exhaustion +- βœ… Message retention works correctly +- βœ… Performance meets success criteria (<2s latency, 99.9% delivery) +- βœ… Documentation complete +- βœ… Docker images published +- βœ… npm package published + +--- + +## Parallel Execution Opportunities + +### Per User Story + +**User Story 1** (Phase 3): +- GitHub integration (T025-T028) can run parallel to Channel implementations (T034-T036) +- Task management (T029-T031) can run parallel to Agent Registry (T032-T033) + +**User Story 4** (Phase 4): +- Authentication tasks (T040-T044) are independent per channel, can parallelize +- Relay Server tasks (T047-T048) can run parallel to MCP Server tasks + +**User Story 5** (Phase 5): +- Capability management (T050-T052) can run parallel to Lifecycle management (T053-T055) + +**User Story 2** (Phase 6): +- All command implementations (T058-T060) can run in parallel +- Text UI components (T061-T062) can run parallel to Observer mode (T063-T064) + +**User Story 3** (Phase 7): +- All command implementations (T066-T068) can run in parallel +- All UI components (T069-T071) can run in parallel + +**Polish** (Phase 8): +- All error handling tasks (T075-T078) can run in parallel +- All optimization tasks (T081-T082) can run in parallel +- Documentation tasks (T083-T084) can run in parallel + +--- + +## MVP Scope Recommendation + +**Minimum Viable Product**: Phases 1-4 (Tasks T001-T049) + +This delivers: +- βœ… Core agent coordination (User Story 1) +- βœ… Secure communication (User Story 4) +- βœ… GitHub integration +- βœ… Multi-channel support (Discord, SignalR, Redis) +- βœ… Task assignment and lifecycle +- βœ… Agent registry and roles + +**MVP Test Command**: `npm run test:integration -- --grep="MVP"` + +**MVP Delivery Time**: ~40-60 hours of focused development + +--- + +## Task Format Validation + +βœ… All 87 tasks follow the required checklist format: +- `- [ ]` checkbox prefix +- Sequential task IDs (T001-T087) +- [P] marker on 42 parallelizable tasks +- [US#] label on 39 user story tasks +- Clear descriptions with file paths +- Organized by phase/user story + +**Format Example**: +``` +- [ ] T034 [P] [US1] Implement DiscordChannel in packages/mcp-server/src/channels/discord/DiscordChannel.ts +``` + +--- + +## Next Steps + +1. **Review task breakdown**: Verify tasks align with specification +2. **Begin implementation**: Start with Phase 1 (Setup) +3. **Track progress**: Check off tasks as completed +4. **Run tests**: Execute integration tests after each phase +5. **Deploy MVP**: After Phase 4 completion + +**Recommended command**: `/speckit.implement` to begin execution + +--- + +## Summary + +- **Total Tasks**: 87 +- **MVP Tasks**: 49 (Phases 1-4) +- **Parallelizable**: 42 tasks marked [P] +- **User Stories**: 5 stories, each independently testable +- **Estimated MVP Time**: 40-60 hours +- **Estimated Full Completion**: 80-120 hours + +**All tasks ready for execution. Proceed with `/speckit.implement` to begin development.**