diff --git a/README.md b/README.md index 8282568..c954b21 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,15 @@ # Cursor Plugins -A collection of official Cursor plugins for the most popular SaaS products and developer tools. Each plugin provides rules, agents, skills, hooks, and MCP server integrations to enhance your development workflow. +A collection of Cursor plugins for popular SaaS products and developer tools. Each plugin provides skills and MCP server integrations. ## Plugins | Plugin | Category | Description | |:-------|:---------|:------------| | [GitHub](plugins/github/) | Developer Tools | Actions, API, CLI, Pull Requests, and repository management | -| [Stripe](plugins/stripe/) | SaaS | Payment processing, subscriptions, webhooks, and billing | -| [Vercel](plugins/vercel/) | Deployment | Serverless functions, Edge Runtime, and project configuration | -| [Supabase](plugins/supabase/) | Backend | Postgres, authentication, storage, realtime, and Edge Functions | | [Sentry](plugins/sentry/) | Observability | Error monitoring, performance tracking, and alerting | -| [Cloudflare](plugins/cloudflare/) | Infrastructure | Workers, Pages, R2, D1, KV, and edge computing | -| [AWS](plugins/aws/) | Infrastructure | Lambda, S3, DynamoDB, CDK, IAM, and cloud infrastructure | | [Docker](plugins/docker/) | Developer Tools | Dockerfiles, Compose, multi-stage builds, and containers | -| [Prisma](plugins/prisma/) | Backend | ORM, schema design, migrations, and database management | | [Firebase](plugins/firebase/) | Backend | Firestore, Cloud Functions, Authentication, and Hosting | -| [Datadog](plugins/datadog/) | Observability | APM, logging, metrics, monitors, and dashboards | | [Twilio](plugins/twilio/) | SaaS | SMS, Voice, WhatsApp, Verify, and communications APIs | | [Slack](plugins/slack/) | SaaS | Bolt framework, Block Kit, Events API, and app development | | [LaunchDarkly](plugins/launchdarkly/) | Developer Tools | Feature flags, experimentation, and progressive rollouts | @@ -28,10 +21,6 @@ A collection of official Cursor plugins for the most popular SaaS products and d agent install ``` -## Plugin Structure - -Each plugin follows the standard Cursor plugin structure. See [plugins/README.md](plugins/README.md) for details. - ## License MIT diff --git a/plugins/README.md b/plugins/README.md index 65399c6..925e37a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1,141 +1,35 @@ # Cursor Plugins -This directory contains official plugins for Cursor. +This directory contains plugins for Cursor. Each plugin provides skills and an MCP server integration. ## Plugin Structure -Each plugin follows a standard structure: - ``` plugin-name/ ├── .cursor/ │ └── plugin.json # Plugin manifest (required) -├── README.md # Plugin documentation -├── rules/ # Cursor rules (.mdc files) -├── agents/ # Subagents (markdown files) ├── skills/ # Agent skills (directories with SKILL.md) -├── hooks/ -│ └── hooks.json # Hook configuration -├── mcp.json # MCP server definitions (optional) -├── extensions/ # VS Code extensions (.vsix files) -├── scripts/ # Utility scripts for hooks -├── LICENSE -└── CHANGELOG.md -``` - -## Plugin Components - -| Component | Location | Purpose | -|:----------------|:----------------------|:-----------------------------------------| -| **Manifest** | `.cursor/plugin.json` | Required metadata file | -| **Rules** | `rules/` | Cursor rules (.mdc files) | -| **Agents** | `agents/` | Subagent Markdown files | -| **Skills** | `skills/` | Agent Skills with SKILL.md files | -| **Hooks** | `hooks/hooks.json` | Hook configuration | -| **MCP servers** | `mcp.json` | MCP server definitions | -| **Extensions** | `extensions/` | VS Code extension bundles (.vsix files) | - -## Creating a Plugin - -1. Create a new directory under `plugins/` -2. Add a `.cursor/plugin.json` manifest file -3. Add your components (rules, agents, skills, hooks, etc.) -4. Add a `README.md` documenting your plugin - -See the `boilerplate` directory for a complete example. - -## Plugin Manifest - -The `.cursor/plugin.json` manifest is required and defines your plugin's metadata: - -```json -{ - "name": "my-plugin", - "version": "1.0.0", - "description": "A brief description of what your plugin does", - "author": { - "name": "Your Name", - "email": "you@example.com" - }, - "license": "MIT", - "keywords": ["example"], - "agents": "./agents/", - "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" -} +├── mcp.json # MCP server definitions +├── README.md +├── CHANGELOG.md +└── LICENSE ``` ## Available Plugins -### Template - -| Plugin | Category | Description | -|:--------------|:------------|:---------------------------------------------------------| -| `boilerplate` | utilities | Complete example plugin demonstrating all components | - -### Developer Tools - -| Plugin | Category | Description | -|:----------------|:-----------------|:-------------------------------------------------------------------------| -| `github` | developer-tools | GitHub Actions, API, CLI, Pull Requests, and repository management | -| `docker` | developer-tools | Dockerfiles, Compose, multi-stage builds, and container best practices | -| `launchdarkly` | developer-tools | Feature flags, experimentation, progressive rollouts, and targeting | - -### Backend & Database - -| Plugin | Category | Description | -|:------------|:---------|:-----------------------------------------------------------------------------| -| `prisma` | backend | ORM, schema design, migrations, and database management | -| `supabase` | backend | Postgres database, authentication, storage, realtime, and Edge Functions | -| `firebase` | backend | Firestore, Authentication, Cloud Functions, Hosting, and Storage | -| `mongodb` | backend | Schema design, queries, aggregation, indexes, and Mongoose ODM | - -### Infrastructure & Deployment - -| Plugin | Category | Description | -|:--------------|:----------------|:-----------------------------------------------------------------------| -| `aws` | infrastructure | Lambda, S3, DynamoDB, CDK, IAM, and cloud infrastructure | -| `cloudflare` | infrastructure | Workers, Pages, R2, D1, KV, and edge computing | -| `vercel` | deployment | Deployments, serverless functions, Edge Runtime, and project config | - -### SaaS & APIs - -| Plugin | Category | Description | -|:----------|:---------|:-------------------------------------------------------------------------| -| `stripe` | saas | Payment processing, subscriptions, webhooks, and billing integration | -| `twilio` | saas | SMS, Voice, WhatsApp, Verify, and communications APIs | -| `slack` | saas | Bolt framework, Block Kit, Events API, and Slack app development | - -### Observability - -| Plugin | Category | Description | -|:----------|:---------------|:---------------------------------------------------------------------| -| `sentry` | observability | Error monitoring, performance tracking, session replay, and alerting | -| `datadog` | observability | APM, logging, metrics, monitors, and observability | +| Plugin | Category | Description | +|:-------|:---------|:------------| +| `github` | Developer Tools | Actions, API, CLI, Pull Requests, and repository management | +| `sentry` | Observability | Error monitoring, performance tracking, and alerting | +| `docker` | Developer Tools | Dockerfiles, Compose, multi-stage builds, and containers | +| `firebase` | Backend | Firestore, Cloud Functions, Authentication, and Hosting | +| `twilio` | SaaS | SMS, Voice, WhatsApp, Verify, and communications APIs | +| `slack` | SaaS | Bolt framework, Block Kit, Events API, and app development | +| `launchdarkly` | Developer Tools | Feature flags, experimentation, and progressive rollouts | +| `mongodb` | Backend | Schema design, queries, aggregation, indexes, and Mongoose | ## Installation -Plugins can be installed via CLI: - ```bash agent install ``` - -Or from a marketplace: - -```bash -agent install @ -``` - -## Contributing - -When adding a new plugin: - -1. Follow the standard directory structure -2. Include a comprehensive README.md -3. Use semantic versioning (MAJOR.MINOR.PATCH) -4. Include a LICENSE file -5. Test your plugin thoroughly before submitting diff --git a/plugins/docker/.cursor/plugin.json b/plugins/docker/.cursor/plugin.json index 8d21087..d664689 100644 --- a/plugins/docker/.cursor/plugin.json +++ b/plugins/docker/.cursor/plugin.json @@ -1,18 +1,27 @@ { "name": "docker", "version": "1.0.0", - "description": "Cursor plugin for Docker — Dockerfiles, Compose, multi-stage builds, and container best practices", - "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "description": "Cursor plugin for Docker \u2014 Dockerfiles, Compose, multi-stage builds, and container best practices", + "author": { + "name": "Cursor", + "email": "plugins@cursor.com" + }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["docker", "containers", "dockerfile", "compose", "devops"], + "keywords": [ + "docker", + "containers", + "dockerfile", + "compose", + "devops" + ], "category": "developer-tools", - "tags": ["containers", "devops", "infrastructure"], - "agents": "./agents/", + "tags": [ + "containers", + "devops", + "infrastructure" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/docker/CHANGELOG.md b/plugins/docker/CHANGELOG.md index 8e8aca9..5f2859d 100644 --- a/plugins/docker/CHANGELOG.md +++ b/plugins/docker/CHANGELOG.md @@ -1,22 +1,13 @@ # Changelog -All notable changes to the Docker plugin for Cursor will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-08 +## [Unreleased] -### Added +## [1.0.0] - 2026-02-07 -- **Plugin manifest** (`.cursor/plugin.json`) with full metadata and file references. -- **Dockerfile best practices rule** (`rules/dockerfile.mdc`) covering multi-stage builds, layer optimization, security, health checks, base image pinning, and `.dockerignore`. -- **Docker Compose best practices rule** (`rules/docker-compose.mdc`) covering named volumes, resource limits, health checks, `depends_on` conditions, environment management, networking, and profiles. -- **Docker Optimization Agent** (`agents/docker-optimization-agent.md`) for analyzing and improving Docker setups — image size reduction, build speed, security hardening, and runtime performance. -- **Containerize App skill** (`skills/containerize-app/SKILL.md`) with production-ready Dockerfile templates for Node.js, Next.js, Python, Go, Java (Spring Boot), and Rust. -- **Setup Docker Compose skill** (`skills/setup-docker-compose/SKILL.md`) for generating multi-service development environments with databases, caches, message queues, and dev tools. -- **Lint hooks** (`hooks/hooks.json`) for pre-commit Dockerfile linting, Compose validation, `.dockerignore` verification, and `:latest` tag detection. -- **Docker lint script** (`scripts/docker-lint.sh`) providing hadolint integration, basic Dockerfile checks, Compose validation, and vulnerability scanning. -- **MCP server configuration** (`mcp.json`) for the Docker MCP server. -- **README** with full documentation of all plugin features. -- **MIT License** (Cursor, 2026). +### Added +- Initial plugin with skills and MCP server configuration diff --git a/plugins/docker/README.md b/plugins/docker/README.md index dd7f875..ea98664 100644 --- a/plugins/docker/README.md +++ b/plugins/docker/README.md @@ -1,129 +1,28 @@ -# Docker Plugin for Cursor +# Docker Plugin -A comprehensive Cursor plugin for Docker development — write better Dockerfiles, Compose files, and containerize applications following production-grade best practices. - -## Features - -- **Dockerfile Best Practices** — Automatic rules for multi-stage builds, layer optimization, security hardening, and image size reduction. -- **Docker Compose Best Practices** — Rules for named volumes, health checks, networking, resource limits, and service dependencies. -- **Docker Optimization Agent** — AI agent that analyzes your Docker setup and provides actionable optimization recommendations. -- **Containerize App Skill** — Generate production-ready Dockerfiles for Node.js, Python, Go, Java, and Rust applications. -- **Docker Compose Setup Skill** — Generate complete multi-service development environments with databases, caches, and queues. -- **Lint Hooks** — Pre-commit hooks for Dockerfile linting (hadolint), Compose validation, and `:latest` tag detection. -- **Docker MCP Server** — Manage containers, images, volumes, and networks directly from Cursor. - -## Directory Structure - -``` -plugins/docker/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── docker-optimization-agent.md # Optimization agent -├── extensions/ # Future extensions -├── hooks/ -│ └── hooks.json # Pre-commit lint hooks -├── rules/ -│ ├── dockerfile.mdc # Dockerfile best practices -│ └── docker-compose.mdc # Compose best practices -├── scripts/ -│ └── docker-lint.sh # Lint and validation script -├── skills/ -│ ├── containerize-app/ -│ │ └── SKILL.md # Containerize an application -│ └── setup-docker-compose/ -│ └── SKILL.md # Set up Docker Compose -├── mcp.json # MCP server configuration -├── CHANGELOG.md -├── LICENSE -└── README.md -``` +Cursor plugin for Docker — Dockerfiles, Compose, multi-stage builds, and container best practices. ## Installation -This plugin is loaded automatically when placed in the `plugins/docker/` directory of a Cursor-enabled workspace. - -## Rules - -Rules are applied automatically when editing matching files: - -| Rule | Globs | Description | -|--------------------|-------------------------------------------------------|--------------------------------------| -| `dockerfile.mdc` | `Dockerfile*`, `**/*.dockerfile`, `**/Dockerfile*` | Dockerfile writing best practices | -| `docker-compose.mdc` | `docker-compose*.yml`, `compose*.yml`, etc. | Docker Compose best practices | - -## Agent - -The **Docker Optimization Agent** (`agents/docker-optimization-agent.md`) specializes in: - -- Reducing Docker image sizes (base image selection, multi-stage builds, layer cleanup) -- Improving build times (layer ordering, BuildKit cache mounts, CI/CD caching) -- Security hardening (non-root users, secret management, vulnerability scanning) -- Runtime performance (resource limits, signal handling, logging) - -## Skills - -### Containerize an Application - -Generates a production-ready, multi-stage Dockerfile for your project. - -**Supported languages:** Node.js, Python, Go, Java, Rust, Ruby, PHP - -**What you get:** -- Multi-stage Dockerfile optimized for your language/framework -- `.dockerignore` file -- Health checks and security hardening -- Build cache optimization - -### Set Up Docker Compose - -Generates a complete Docker Compose configuration for multi-service environments. - -**Supported services:** PostgreSQL, MySQL, MongoDB, Redis, RabbitMQ, Kafka, Elasticsearch, MinIO, NATS - -**What you get:** -- `compose.yaml` with all required services -- `compose.override.yaml` for development -- `.env.example` template -- Health checks, named volumes, and proper networking - -## Hooks - -Pre-commit hooks run automatically to catch issues early: - -| Hook | Event | Description | -|------------------------|--------------|--------------------------------------------| -| `dockerfile-lint` | pre-commit | Lint Dockerfiles with hadolint | -| `compose-validate` | pre-commit | Validate Compose file syntax | -| `dockerignore-check` | pre-commit | Ensure .dockerignore exists | -| `no-latest-tag` | pre-commit | Detect `:latest` tag usage | -| `security-scan` | post-build | Vulnerability scan (disabled by default) | - -## Scripts - -The `scripts/docker-lint.sh` script provides the linting backend: - ```bash -# Lint a Dockerfile -./scripts/docker-lint.sh lint-dockerfile Dockerfile +agent install docker +``` -# Validate a Compose file -./scripts/docker-lint.sh lint-compose compose.yaml +## Components -# Check for .dockerignore -./scripts/docker-lint.sh check-dockerignore +### Skills -# Check for :latest tags -./scripts/docker-lint.sh check-no-latest Dockerfile compose.yaml +| Skill | Description | +|:------|:------------| +| `containerize-app` | Language-specific Dockerfiles for Node.js, Python, Go, Java, and Rust with multi-stage builds | +| `setup-docker-compose` | Multi-service development environments with databases, caches, and queues | -# Run security scan on built images -./scripts/docker-lint.sh security-scan -``` +### MCP Server -## MCP Server +Provides Docker management via `mcp/docker` container image. -The plugin configures the [Docker MCP server](https://github.com/docker/docker-mcp) for direct container management from Cursor, providing tools to manage containers, images, volumes, and networks. +Requires Docker socket access. ## License -MIT — see [LICENSE](./LICENSE). +MIT diff --git a/plugins/docker/agents/docker-optimization-agent.md b/plugins/docker/agents/docker-optimization-agent.md deleted file mode 100644 index ccf99d3..0000000 --- a/plugins/docker/agents/docker-optimization-agent.md +++ /dev/null @@ -1,120 +0,0 @@ -# Docker Optimization Agent - -## Identity - -You are a Docker optimization specialist. You analyze Dockerfiles, Compose configurations, and container setups to reduce image sizes, speed up builds, improve security posture, and optimize runtime performance. - -## Expertise - -- Docker image size reduction and layer optimization -- Multi-stage build strategies for all major languages and frameworks -- Build cache efficiency and CI/CD pipeline optimization -- Container security hardening and vulnerability remediation -- Runtime performance tuning and resource management -- Docker Compose architecture and service orchestration - -## Behavior - -When a user asks for help optimizing their Docker setup, follow this workflow: - -### 1. Analyze the Current Setup - -- Read the Dockerfile(s) and Compose files in the project. -- Identify the language/framework and its specific containerization patterns. -- Check for a `.dockerignore` file and evaluate its completeness. -- Look for common anti-patterns and inefficiencies. - -### 2. Image Size Optimization - -Evaluate and recommend improvements for image size: - -- **Base image selection**: Suggest moving from full OS images to `alpine`, `slim`, or `distroless` variants. - - `node:20` (1.1 GB) → `node:20-alpine` (180 MB) → `gcr.io/distroless/nodejs20` (130 MB) - - `python:3.12` (1 GB) → `python:3.12-slim` (150 MB) → `python:3.12-alpine` (60 MB) - - `golang:1.22` (800 MB) → build + `scratch` or `gcr.io/distroless/static` (< 20 MB) -- **Multi-stage builds**: Ensure build dependencies do not ship in the final image. -- **Layer cleanup**: Remove package manager caches, temp files, and build artifacts within the same `RUN` layer. -- **Dependency pruning**: Install only production dependencies in the final stage. - -### 3. Build Time Optimization - -Evaluate and recommend improvements for build speed: - -- **Layer ordering**: Ensure the least-frequently-changing layers come first: - 1. System packages - 2. Dependency manifests (package.json, go.mod, requirements.txt) - 3. Dependency installation - 4. Source code copy - 5. Build step -- **BuildKit cache mounts**: Use `--mount=type=cache` for package manager caches: - ```dockerfile - RUN --mount=type=cache,target=/root/.npm npm ci - RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - RUN --mount=type=cache,target=/go/pkg/mod go build -o /app . - ``` -- **Parallel builds**: Use `COPY --link` for parallel layer processing. -- **.dockerignore**: Ensure large directories (node_modules, .git, dist) are excluded to minimize build context transfer time. -- **CI/CD caching**: Recommend registry-based cache or GitHub Actions cache: - ```bash - docker buildx build --cache-from type=registry,ref=ghcr.io/org/app:cache \ - --cache-to type=registry,ref=ghcr.io/org/app:cache,mode=max . - ``` - -### 4. Security Hardening - -Evaluate and recommend security improvements: - -- **Non-root user**: Ensure the container runs as a non-root user. -- **Image scanning**: Recommend integrating `docker scout`, `trivy`, or `grype` into CI. -- **Secret management**: Ensure no secrets are baked into the image. Use build secrets: - ```dockerfile - RUN --mount=type=secret,id=github_token \ - GITHUB_TOKEN=$(cat /run/secrets/github_token) npm ci - ``` -- **Read-only filesystem**: Recommend `read_only: true` in compose where possible. -- **Capability dropping**: Recommend `cap_drop: [ALL]` and adding back only necessary caps. -- **Pinned versions**: Ensure base images are pinned to specific versions or digests. -- **No latest tag**: Flag any use of `:latest` as a security and reproducibility risk. -- **HEALTHCHECK**: Ensure every service has a proper health check defined. - -### 5. Runtime Performance - -Evaluate runtime configuration: - -- **Resource limits**: Recommend appropriate CPU and memory limits. -- **Logging**: Ensure log rotation is configured to prevent disk exhaustion. -- **Networking**: Suggest network segmentation for multi-service setups. -- **Signal handling**: Ensure proper PID 1 handling (use `tini` or `dumb-init` if needed). -- **Graceful shutdown**: Verify that `STOPSIGNAL` and signal handlers are configured. - -### 6. Compose Architecture - -For multi-service setups: - -- **Service dependencies**: Use health-check-based `depends_on` conditions. -- **Named volumes**: Ensure persistent data uses named volumes, not anonymous volumes. -- **Profiles**: Suggest using profiles for dev-only services (debuggers, mail catchers, etc.). -- **Environment management**: Recommend `env_file` over inline environment variables. - -## Output Format - -When providing optimization recommendations, structure your response as: - -1. **Summary**: Brief overview of findings and estimated impact. -2. **Critical Issues**: Security vulnerabilities or major inefficiencies (fix immediately). -3. **Optimizations**: Size, speed, and performance improvements (high impact). -4. **Suggestions**: Minor improvements and best-practice alignment (nice to have). -5. **Optimized Dockerfile/Compose**: Provide the complete rewritten file(s) with inline comments explaining each change. - -Always provide before/after metrics where possible: -- Image size (MB) -- Build time (estimated) -- Number of layers -- Security findings count - -## Constraints - -- Do not break existing functionality. All recommendations must maintain application behavior. -- Prefer widely-adopted, stable tools and practices over bleeding-edge features. -- Consider CI/CD compatibility when recommending BuildKit-specific features. -- Respect the user's technology choices; optimize within the chosen stack rather than recommending a different stack. diff --git a/plugins/docker/hooks/hooks.json b/plugins/docker/hooks/hooks.json deleted file mode 100644 index 49b3189..0000000 --- a/plugins/docker/hooks/hooks.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "name": "dockerfile-lint", - "description": "Lint Dockerfiles using hadolint before committing", - "event": "pre-commit", - "match": { - "files": ["Dockerfile*", "**/*.dockerfile", "**/Dockerfile*"] - }, - "command": { - "run": "bash ./scripts/docker-lint.sh lint-dockerfile ${files}", - "timeout": 30000 - }, - "severity": "warning", - "enabled": true - }, - { - "name": "compose-validate", - "description": "Validate Docker Compose files for syntax and schema correctness", - "event": "pre-commit", - "match": { - "files": [ - "docker-compose*.yml", - "docker-compose*.yaml", - "compose*.yml", - "compose*.yaml" - ] - }, - "command": { - "run": "bash ./scripts/docker-lint.sh lint-compose ${files}", - "timeout": 30000 - }, - "severity": "warning", - "enabled": true - }, - { - "name": "dockerignore-check", - "description": "Verify .dockerignore exists when Dockerfiles are present", - "event": "pre-commit", - "match": { - "files": ["Dockerfile*", "**/*.dockerfile"] - }, - "command": { - "run": "bash ./scripts/docker-lint.sh check-dockerignore", - "timeout": 10000 - }, - "severity": "info", - "enabled": true - }, - { - "name": "no-latest-tag", - "description": "Check that no Docker images use the :latest tag", - "event": "pre-commit", - "match": { - "files": [ - "Dockerfile*", - "**/*.dockerfile", - "docker-compose*.yml", - "docker-compose*.yaml", - "compose*.yml", - "compose*.yaml" - ] - }, - "command": { - "run": "bash ./scripts/docker-lint.sh check-no-latest ${files}", - "timeout": 10000 - }, - "severity": "warning", - "enabled": true - }, - { - "name": "security-scan", - "description": "Run vulnerability scan on built Docker images", - "event": "post-build", - "match": { - "files": ["Dockerfile*", "**/*.dockerfile"] - }, - "command": { - "run": "bash ./scripts/docker-lint.sh security-scan", - "timeout": 120000 - }, - "severity": "error", - "enabled": false - } - ] -} diff --git a/plugins/docker/rules/docker-compose.mdc b/plugins/docker/rules/docker-compose.mdc deleted file mode 100644 index 17dd0a1..0000000 --- a/plugins/docker/rules/docker-compose.mdc +++ /dev/null @@ -1,238 +0,0 @@ ---- -description: Best practices for Docker Compose configuration files -globs: - - "docker-compose*.yml" - - "docker-compose*.yaml" - - "compose*.yml" - - "compose*.yaml" -alwaysApply: false ---- - -# Docker Compose Best Practices - -## Version and File Structure - -- Use Compose Specification (no `version` key needed for Docker Compose v2+). -- Split configurations by environment using multiple compose files: - ``` - compose.yaml # Base services - compose.override.yaml # Local development overrides (auto-loaded) - compose.prod.yaml # Production overrides - compose.test.yaml # Testing overrides - ``` -- Use `docker compose -f compose.yaml -f compose.prod.yaml up` to layer configurations. - -## Image Versioning - -- **Pin image versions** — never use `latest` in compose files: - ```yaml - services: - db: - image: postgres:16.2-alpine # Good - # image: postgres:latest # Bad - ``` -- For custom-built services, use explicit build contexts and targets: - ```yaml - services: - api: - build: - context: . - dockerfile: Dockerfile - target: production - ``` - -## Named Volumes for Persistence - -- **Use named volumes** for any data that must persist across container restarts: - ```yaml - services: - db: - image: postgres:16.2-alpine - volumes: - - postgres_data:/var/lib/postgresql/data - - volumes: - postgres_data: - driver: local - ``` -- Avoid bind mounts in production. Use them only for development hot-reloading: - ```yaml - services: - app: - volumes: - - ./src:/app/src # Development only - ``` - -## Resource Limits - -- **Set memory and CPU limits** to prevent runaway containers: - ```yaml - services: - api: - deploy: - resources: - limits: - cpus: "1.0" - memory: 512M - reservations: - cpus: "0.25" - memory: 128M - ``` - -## Health Checks - -- **Define health checks** for every service so dependent services can wait: - ```yaml - services: - db: - image: postgres:16.2-alpine - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - ``` - -## Service Dependencies - -- Use `depends_on` with `condition` to control startup order based on health: - ```yaml - services: - api: - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - ``` -- Never rely on `depends_on` alone without health check conditions — it only guarantees container start, not readiness. - -## Environment Variables - -- **Use `env_file`** to manage environment variables instead of inline `environment`: - ```yaml - services: - api: - env_file: - - .env - - .env.local - ``` -- Never commit `.env` files with secrets. Use `.env.example` as a template. -- Use variable substitution for dynamic values: - ```yaml - services: - api: - image: myapp:${APP_VERSION:-latest} - ``` - -## Networking - -- **Define custom networks** for service isolation: - ```yaml - services: - api: - networks: - - frontend - - backend - db: - networks: - - backend - - networks: - frontend: - driver: bridge - backend: - driver: bridge - internal: true # No external access - ``` -- Use `internal: true` for backend networks that should not have internet access. -- Services on the same network can reach each other by service name (built-in DNS). - -## Profiles for Optional Services - -- Use **profiles** to group optional services that are not needed in every environment: - ```yaml - services: - app: - image: myapp:1.0 - # No profile — always starts - - debug: - image: busybox - profiles: - - debug - - monitoring: - image: grafana/grafana:10.3.1 - profiles: - - monitoring - - mailhog: - image: mailhog/mailhog:v1.0.1 - profiles: - - dev - ``` -- Start optional services with `docker compose --profile debug up`. - -## Logging - -- Configure log drivers and limits to prevent disk exhaustion: - ```yaml - services: - api: - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" - ``` - -## Restart Policies - -- Set appropriate restart policies: - ```yaml - services: - api: - restart: unless-stopped # Recommended for production - worker: - restart: on-failure - ``` - -## Security - -- Do not run containers as root. Use `user` directive: - ```yaml - services: - api: - user: "1000:1000" - ``` -- Mount secrets using Docker secrets or bind-mount read-only files: - ```yaml - secrets: - db_password: - file: ./secrets/db_password.txt - - services: - db: - secrets: - - db_password - ``` -- Use `read_only: true` for containers that do not need to write to the filesystem. -- Drop unnecessary Linux capabilities with `cap_drop: [ALL]` and add only what is needed. - -## Development Workflow - -- Use `watch` for file-sync based hot-reloading (Compose v2.22+): - ```yaml - services: - app: - develop: - watch: - - action: sync - path: ./src - target: /app/src - - action: rebuild - path: package.json - ``` -- Use `docker compose watch` for automatic syncing without bind mounts. diff --git a/plugins/docker/rules/dockerfile.mdc b/plugins/docker/rules/dockerfile.mdc deleted file mode 100644 index b8ab84b..0000000 --- a/plugins/docker/rules/dockerfile.mdc +++ /dev/null @@ -1,152 +0,0 @@ ---- -description: Best practices for writing production-grade Dockerfiles -globs: - - "Dockerfile*" - - "**/*.dockerfile" - - "**/Dockerfile*" -alwaysApply: false ---- - -# Dockerfile Best Practices - -## Base Image Selection - -- **Pin base image versions** — never use `latest`. Use specific tags with SHA digests for reproducibility: - ```dockerfile - # Good - FROM node:20.11-alpine3.19@sha256:abcdef1234567890... - # Acceptable - FROM node:20.11-alpine3.19 - # Bad - FROM node:latest - ``` -- Prefer minimal base images (`alpine`, `slim`, `distroless`) to reduce attack surface and image size. -- Use official images from Docker Hub or verified publishers. - -## Multi-Stage Builds - -- **Always use multi-stage builds** for compiled languages and applications with build dependencies: - ```dockerfile - # Build stage - FROM node:20-alpine AS builder - WORKDIR /app - COPY package*.json ./ - RUN npm ci --only=production - COPY . . - RUN npm run build - - # Production stage - FROM node:20-alpine AS production - WORKDIR /app - COPY --from=builder /app/dist ./dist - COPY --from=builder /app/node_modules ./node_modules - USER node - CMD ["node", "dist/index.js"] - ``` -- Name stages clearly (`builder`, `test`, `production`) for readability. -- Copy only necessary artifacts from build stages into the final image. - -## Layer Ordering and Cache Efficiency - -- Order Dockerfile instructions from least to most frequently changing: - 1. Base image and system dependencies - 2. Language runtime configuration - 3. Dependency manifests (`package.json`, `requirements.txt`, `go.mod`) - 4. Install dependencies - 5. Copy application source code - 6. Build step -- **Copy dependency files before source code** to leverage Docker layer caching: - ```dockerfile - COPY package.json package-lock.json ./ - RUN npm ci - COPY . . - ``` - -## Minimize Layers - -- Combine related `RUN` commands using `&&` and line continuations: - ```dockerfile - RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - curl \ - ca-certificates && \ - rm -rf /var/lib/apt/lists/* - ``` -- Clean up caches and temporary files in the same layer they are created. - -## Security - -- **Run as non-root** — always include a `USER` instruction: - ```dockerfile - RUN addgroup -S appgroup && adduser -S appuser -G appgroup - USER appuser - ``` -- Do not store secrets in the image. Use build secrets or runtime injection: - ```dockerfile - RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci - ``` -- Scan images for vulnerabilities using `docker scout`, `trivy`, or `grype`. -- Drop all capabilities and add only what is needed at runtime. -- Set `HEALTHCHECK` to enable orchestrator health monitoring: - ```dockerfile - HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:3000/health || exit 1 - ``` - -## Use COPY Instead of ADD - -- Prefer `COPY` over `ADD` unless you specifically need tar extraction or URL fetching. -- `COPY` is explicit and predictable; `ADD` has implicit behaviors that can introduce surprises. - -## Build Arguments and Labels - -- Use `ARG` for configurable builds: - ```dockerfile - ARG NODE_ENV=production - ENV NODE_ENV=${NODE_ENV} - ``` -- Add OCI-compliant labels for metadata: - ```dockerfile - LABEL org.opencontainers.image.source="https://github.com/org/repo" - LABEL org.opencontainers.image.version="1.0.0" - LABEL org.opencontainers.image.description="My application" - ``` - -## .dockerignore - -- **Always include a `.dockerignore`** file to exclude unnecessary files from the build context: - ``` - node_modules - .git - .env - *.md - .github - coverage - dist - .dockerignore - Dockerfile - docker-compose*.yml - ``` - -## Entrypoint and CMD - -- Use `ENTRYPOINT` for the main executable and `CMD` for default arguments: - ```dockerfile - ENTRYPOINT ["node"] - CMD ["dist/index.js"] - ``` -- Prefer exec form (`["executable", "arg"]`) over shell form to ensure proper signal handling. -- Use `tini` or `dumb-init` as PID 1 if the application does not handle signals: - ```dockerfile - RUN apk add --no-cache tini - ENTRYPOINT ["/sbin/tini", "--"] - CMD ["node", "dist/index.js"] - ``` - -## Miscellaneous - -- Set `WORKDIR` explicitly — do not rely on the default. -- Avoid installing unnecessary packages. Use `--no-install-recommends` with apt. -- Use `.env` files or Docker secrets for environment-specific configuration, not hardcoded `ENV` values. -- Keep images small: target under 100 MB for microservices, under 500 MB for full-stack apps. -- Tag images with git SHA and semantic version for traceability. diff --git a/plugins/docker/scripts/docker-lint.sh b/plugins/docker/scripts/docker-lint.sh deleted file mode 100755 index 2467d39..0000000 --- a/plugins/docker/scripts/docker-lint.sh +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/env bash -# -# docker-lint.sh — Linting and validation utilities for Docker files. -# Used by the Docker Cursor plugin hooks. -# -set -euo pipefail - -# Colors for output -RED='\033[0;31m' -YELLOW='\033[0;33m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# ------------------------------------------------------------------- -# Helpers -# ------------------------------------------------------------------- - -info() { echo -e "${BLUE}[info]${NC} $*"; } -success() { echo -e "${GREEN}[pass]${NC} $*"; } -warn() { echo -e "${YELLOW}[warn]${NC} $*"; } -error() { echo -e "${RED}[fail]${NC} $*"; } - -command_exists() { command -v "$1" &>/dev/null; } - -# ------------------------------------------------------------------- -# lint-dockerfile: Lint one or more Dockerfiles with hadolint -# ------------------------------------------------------------------- -lint_dockerfile() { - local files=("${@}") - local exit_code=0 - - if [ ${#files[@]} -eq 0 ]; then - warn "No Dockerfile paths provided." - return 0 - fi - - for file in "${files[@]}"; do - if [ ! -f "$file" ]; then - warn "File not found: $file" - continue - fi - - info "Linting $file ..." - - if command_exists hadolint; then - if hadolint --no-color "$file"; then - success "$file passed hadolint checks." - else - warn "$file has hadolint warnings/errors." - exit_code=1 - fi - elif command_exists docker; then - if docker run --rm -i hadolint/hadolint < "$file"; then - success "$file passed hadolint checks." - else - warn "$file has hadolint warnings/errors." - exit_code=1 - fi - else - warn "hadolint is not installed. Falling back to basic checks for $file." - basic_dockerfile_lint "$file" || exit_code=1 - fi - done - - return $exit_code -} - -# ------------------------------------------------------------------- -# basic_dockerfile_lint: Simple pattern-based Dockerfile checks -# ------------------------------------------------------------------- -basic_dockerfile_lint() { - local file="$1" - local exit_code=0 - - # Check for :latest tag in FROM instructions - if grep -Pn '^FROM\s+\S+:latest' "$file" 2>/dev/null; then - warn "$file: Uses ':latest' tag in FROM instruction. Pin to a specific version." - exit_code=1 - fi - - # Check for missing USER instruction - if ! grep -qP '^\s*USER\s+' "$file" 2>/dev/null; then - warn "$file: No USER instruction found. Container will run as root." - exit_code=1 - fi - - # Check for ADD instead of COPY - if grep -Pn '^\s*ADD\s+(?!https?://)' "$file" 2>/dev/null; then - warn "$file: Uses ADD instead of COPY. Prefer COPY unless extracting archives." - exit_code=1 - fi - - # Check for missing HEALTHCHECK - if ! grep -qP '^\s*HEALTHCHECK\s+' "$file" 2>/dev/null; then - warn "$file: No HEALTHCHECK instruction found." - fi - - # Check for apt-get without cleanup - if grep -qP 'apt-get install' "$file" && ! grep -qP 'rm -rf /var/lib/apt/lists' "$file"; then - warn "$file: apt-get install without cleanup. Add 'rm -rf /var/lib/apt/lists/*'." - exit_code=1 - fi - - if [ $exit_code -eq 0 ]; then - success "$file passed basic checks." - fi - - return $exit_code -} - -# ------------------------------------------------------------------- -# lint-compose: Validate Docker Compose files -# ------------------------------------------------------------------- -lint_compose() { - local files=("${@}") - local exit_code=0 - - if [ ${#files[@]} -eq 0 ]; then - warn "No Compose file paths provided." - return 0 - fi - - for file in "${files[@]}"; do - if [ ! -f "$file" ]; then - warn "File not found: $file" - continue - fi - - info "Validating $file ..." - - # Syntax validation with docker compose - if command_exists docker; then - if docker compose -f "$file" config --quiet 2>/dev/null; then - success "$file is valid." - else - error "$file has syntax errors." - docker compose -f "$file" config 2>&1 || true - exit_code=1 - fi - else - warn "docker not available. Running basic YAML checks on $file." - basic_compose_lint "$file" || exit_code=1 - fi - done - - return $exit_code -} - -# ------------------------------------------------------------------- -# basic_compose_lint: Simple checks for Compose files -# ------------------------------------------------------------------- -basic_compose_lint() { - local file="$1" - local exit_code=0 - - # Check for :latest tags - if grep -Pn 'image:\s*\S+:latest' "$file" 2>/dev/null; then - warn "$file: Uses ':latest' image tag. Pin to a specific version." - exit_code=1 - fi - - # Check for missing healthcheck - if ! grep -q 'healthcheck:' "$file"; then - warn "$file: No healthcheck defined for any service." - fi - - # Check for resource limits - if ! grep -q 'resources:' "$file" && ! grep -q 'deploy:' "$file"; then - warn "$file: No resource limits defined." - fi - - if [ $exit_code -eq 0 ]; then - success "$file passed basic checks." - fi - - return $exit_code -} - -# ------------------------------------------------------------------- -# check-dockerignore: Verify .dockerignore exists -# ------------------------------------------------------------------- -check_dockerignore() { - local project_root - project_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" - - if [ -f "$project_root/.dockerignore" ]; then - success ".dockerignore exists at $project_root/.dockerignore" - - # Check for common entries - local missing=() - for entry in ".git" "node_modules" ".env"; do - if ! grep -q "$entry" "$project_root/.dockerignore"; then - missing+=("$entry") - fi - done - - if [ ${#missing[@]} -gt 0 ]; then - warn ".dockerignore is missing recommended entries: ${missing[*]}" - fi - else - warn "No .dockerignore found at project root ($project_root). Consider creating one." - return 1 - fi -} - -# ------------------------------------------------------------------- -# check-no-latest: Ensure no :latest tags are used -# ------------------------------------------------------------------- -check_no_latest() { - local files=("${@}") - local exit_code=0 - - for file in "${files[@]}"; do - if [ ! -f "$file" ]; then - continue - fi - - # Check Dockerfiles - if echo "$file" | grep -iqE 'dockerfile'; then - if grep -Pn '^FROM\s+\S+:latest' "$file" 2>/dev/null; then - warn "$file: FROM uses ':latest' tag." - exit_code=1 - fi - fi - - # Check Compose files - if echo "$file" | grep -iqE 'compose|docker-compose'; then - if grep -Pn 'image:\s*\S+:latest' "$file" 2>/dev/null; then - warn "$file: Service image uses ':latest' tag." - exit_code=1 - fi - fi - done - - if [ $exit_code -eq 0 ]; then - success "No ':latest' tags found." - fi - - return $exit_code -} - -# ------------------------------------------------------------------- -# security-scan: Scan Docker images for vulnerabilities -# ------------------------------------------------------------------- -security_scan() { - info "Running security scan on built images..." - - if command_exists trivy; then - info "Using trivy for vulnerability scanning." - local images - images=$(docker images --format '{{.Repository}}:{{.Tag}}' --filter 'dangling=false' | head -5) - for image in $images; do - info "Scanning $image ..." - trivy image --severity HIGH,CRITICAL "$image" - done - elif command_exists docker && docker scout version &>/dev/null 2>&1; then - info "Using Docker Scout for vulnerability scanning." - local images - images=$(docker images --format '{{.Repository}}:{{.Tag}}' --filter 'dangling=false' | head -5) - for image in $images; do - info "Scanning $image ..." - docker scout cves "$image" --only-severity critical,high - done - elif command_exists grype; then - info "Using grype for vulnerability scanning." - local images - images=$(docker images --format '{{.Repository}}:{{.Tag}}' --filter 'dangling=false' | head -5) - for image in $images; do - info "Scanning $image ..." - grype "$image" --only-fixed --fail-on high - done - else - error "No vulnerability scanner found. Install trivy, grype, or Docker Scout." - return 1 - fi -} - -# ------------------------------------------------------------------- -# Main dispatcher -# ------------------------------------------------------------------- -main() { - local command="${1:-help}" - shift || true - - case "$command" in - lint-dockerfile) lint_dockerfile "$@" ;; - lint-compose) lint_compose "$@" ;; - check-dockerignore) check_dockerignore ;; - check-no-latest) check_no_latest "$@" ;; - security-scan) security_scan ;; - help|--help|-h) - echo "Usage: docker-lint.sh [files...]" - echo "" - echo "Commands:" - echo " lint-dockerfile Lint Dockerfiles with hadolint" - echo " lint-compose Validate Docker Compose files" - echo " check-dockerignore Verify .dockerignore exists" - echo " check-no-latest Check for :latest tag usage" - echo " security-scan Scan images for vulnerabilities" - echo " help Show this help message" - ;; - *) - error "Unknown command: $command" - main help - return 1 - ;; - esac -} - -main "$@" diff --git a/plugins/firebase/.cursor/plugin.json b/plugins/firebase/.cursor/plugin.json index 3dc7403..c2149de 100644 --- a/plugins/firebase/.cursor/plugin.json +++ b/plugins/firebase/.cursor/plugin.json @@ -1,18 +1,27 @@ { "name": "firebase", "version": "1.0.0", - "description": "Cursor plugin for Firebase — Firestore, Authentication, Cloud Functions, Hosting, and Storage", - "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "description": "Cursor plugin for Firebase \u2014 Firestore, Authentication, Cloud Functions, Hosting, and Storage", + "author": { + "name": "Cursor", + "email": "plugins@cursor.com" + }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["firebase", "firestore", "cloud-functions", "authentication", "hosting"], + "keywords": [ + "firebase", + "firestore", + "cloud-functions", + "authentication", + "hosting" + ], "category": "backend", - "tags": ["baas", "google-cloud", "real-time"], - "agents": "./agents/", + "tags": [ + "baas", + "google-cloud", + "real-time" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/firebase/CHANGELOG.md b/plugins/firebase/CHANGELOG.md index c7e527f..5f2859d 100644 --- a/plugins/firebase/CHANGELOG.md +++ b/plugins/firebase/CHANGELOG.md @@ -1,21 +1,13 @@ # Changelog -All notable changes to the Firebase Cursor Plugin will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-08 +## [Unreleased] -### Added +## [1.0.0] - 2026-02-07 -- Firestore best practices rule (`rules/firestore.mdc`) covering data modeling, batch writes, transactions, indexes, pagination, timestamps, converters, offline persistence, and cost optimization. -- Firebase security rules guide (`rules/firebase-security-rules.mdc`) covering authentication, data validation, custom claims, RBAC, helper functions, Storage rules, and emulator testing. -- Firebase Architect Agent (`agents/firebase-architect-agent.md`) for schema design, Cloud Functions architecture, security rules, authentication strategy, cost optimization, and scaling guidance. -- Setup Firebase skill (`skills/setup-firebase/SKILL.md`) with CLI installation, project initialization, emulator configuration, SDK setup, and deployment steps. -- Design Firestore Schema skill (`skills/design-firestore-schema/SKILL.md`) with data modeling patterns, denormalization, security rules, composite indexes, and typed data access layer. -- Hooks configuration (`hooks/hooks.json`) for validating security rules on save, linting Cloud Functions, running rule tests on commit, and deploying on deploy events. -- MCP server configuration (`mcp.json`) for Firebase integration. -- Deployment script (`scripts/deploy-firebase.sh`) with target selection and prerequisite validation. -- Plugin manifest (`.cursor/plugin.json`). -- README, CHANGELOG, and MIT LICENSE. +### Added +- Initial plugin with skills and MCP server configuration diff --git a/plugins/firebase/README.md b/plugins/firebase/README.md index 71c4283..475f301 100644 --- a/plugins/firebase/README.md +++ b/plugins/firebase/README.md @@ -1,89 +1,28 @@ -# Firebase Cursor Plugin +# Firebase Plugin -Cursor plugin for **Firebase** — Firestore, Authentication, Cloud Functions, Hosting, and Storage. +Cursor plugin for Firebase — Firestore, Authentication, Cloud Functions, Hosting, and Storage. -## Features +## Installation -- **Rules** — Best-practice rules for Firestore data modeling, security rules, and query patterns -- **Agent** — Firebase architect agent for schema design, Cloud Functions, and cost optimization guidance -- **Skills** — Step-by-step workflows for project setup, Firestore schema design, and deployment -- **Hooks** — Auto-validation of security rules and Cloud Functions on save, commit, and deploy -- **MCP** — Firebase MCP server integration for Firestore, Auth, and Cloud Functions management - -## Plugin Structure - -``` -plugins/firebase/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── firebase-architect-agent.md # Architecture & design agent -├── rules/ -│ ├── firestore.mdc # Firestore data modeling & query rules -│ └── firebase-security-rules.mdc # Security rules best practices -├── skills/ -│ ├── setup-firebase/ -│ │ └── SKILL.md # Firebase CLI setup & project initialization -│ └── design-firestore-schema/ -│ └── SKILL.md # Firestore schema design patterns -├── hooks/ -│ └── hooks.json # Save, commit, and deploy hooks -├── scripts/ -│ └── deploy-firebase.sh # Deployment helper script -├── extensions/ # Extension directory (reserved) -├── mcp.json # MCP server configuration -├── README.md -├── CHANGELOG.md -└── LICENSE +```bash +agent install firebase ``` -## Getting Started - -### Prerequisites - -- [Cursor](https://cursor.com/) editor -- [Node.js](https://nodejs.org/) 18+ -- [Firebase CLI](https://firebase.google.com/docs/cli) (`npm install -g firebase-tools`) -- A Firebase project (create one at [Firebase Console](https://console.firebase.google.com)) - -### Installation - -This plugin is part of the [service-plugin-generation](https://github.com/cursor/service-plugin-generation) repository. Clone the repo and the plugin will be available in Cursor automatically. +## Components -### Usage - -1. **Follow the setup skill** — Use `skills/setup-firebase/SKILL.md` to initialize a new Firebase project with CLI, emulators, and deployment configuration. - -2. **Design your schema** — Use `skills/design-firestore-schema/SKILL.md` for data modeling patterns, security rules, and composite indexes. - -3. **Ask the architect agent** — Invoke the Firebase Architect Agent for guidance on schema design, Cloud Functions architecture, cost optimization, and scaling. - -4. **Deploy** — Use the deploy script for targeted or full deployments: - -```bash -./scripts/deploy-firebase.sh # Deploy everything -./scripts/deploy-firebase.sh functions # Deploy Cloud Functions only -./scripts/deploy-firebase.sh firestore hosting # Deploy Firestore rules + Hosting -``` +### Skills -## Services Covered +| Skill | Description | +|:------|:------------| +| `setup-firebase` | Project initialization, SDK setup, emulator configuration, and deployment | +| `design-firestore-schema` | Data modeling patterns, subcollections, denormalization, and security rules | -| Service | Coverage | -|---------|----------| -| **Firestore** | Data modeling, queries, indexes, offline persistence, converters | -| **Authentication** | Providers, custom claims, RBAC, session management | -| **Cloud Functions** | Triggers, HTTP callables, scheduling, idempotency, cold starts | -| **Hosting** | Static deployment, CDN caching, multi-site | -| **Cloud Storage** | Upload rules, file validation, security | +### MCP Server -## Links +Provides Firebase management via `firebase-mcp-server`. -- [Firebase Documentation](https://firebase.google.com/docs) -- [Firestore Data Modeling](https://firebase.google.com/docs/firestore/data-model) -- [Firebase Security Rules](https://firebase.google.com/docs/rules) -- [Cloud Functions](https://firebase.google.com/docs/functions) -- [Firebase CLI Reference](https://firebase.google.com/docs/cli) +Requires `FIREBASE_TOKEN` environment variable. ## License -MIT — see [LICENSE](./LICENSE). +MIT diff --git a/plugins/firebase/agents/firebase-architect-agent.md b/plugins/firebase/agents/firebase-architect-agent.md deleted file mode 100644 index 4e96d53..0000000 --- a/plugins/firebase/agents/firebase-architect-agent.md +++ /dev/null @@ -1,89 +0,0 @@ -# Firebase Architect Agent - -You are a Firebase architecture expert. You help developers design, build, and optimize Firebase-based applications spanning Firestore, Authentication, Cloud Functions, Hosting, and Cloud Storage. - -## Core Competencies - -### Firestore Schema Design -- Help design Firestore data models optimized for query patterns rather than relational normalization. -- Advise on denormalization strategies, subcollection structures, and document references. -- Recommend when to use subcollections vs. root collections vs. embedded maps. -- Guide developers on choosing between document-based lookups and collection group queries. -- Identify and suggest composite indexes required by the application's query patterns. - -### Cloud Functions Architecture -- Design Cloud Functions for event-driven workflows: Firestore triggers, Auth triggers, HTTP callable functions, and scheduled functions. -- Advise on function organization: group by feature, use a single entry point (`index.ts`) that re-exports from feature modules. -- Recommend patterns for cold start optimization: keep dependencies minimal, use lazy initialization, prefer v2 functions with concurrency. -- Guide on idempotency — Cloud Functions may be retried, so every function must produce the same result when called multiple times with the same input. -- Advise on using Cloud Tasks, Pub/Sub, and Eventarc for asynchronous and decoupled architectures. - -### Security Rules Design -- Design Firestore and Storage security rules that enforce authentication, authorization, and data validation. -- Recommend role-based access control (RBAC) patterns using Firebase Auth custom claims. -- Help structure rules for multi-tenant applications with organization-scoped data. -- Advise on separating public, authenticated, and admin-only access at the rule level. - -### Authentication Strategy -- Guide on choosing authentication providers: email/password, Google, Apple, phone, anonymous, SAML, OIDC. -- Design flows for linking multiple auth providers to a single user account. -- Advise on session management, token refresh, and custom token generation from backend services. -- Recommend patterns for user onboarding: creating user profiles in Firestore via Auth triggers. - -### Cost Optimization -- Analyze data models for read/write cost efficiency — minimize document reads by caching and using listeners. -- Recommend `select()` field masks in Admin SDK queries to reduce bandwidth. -- Advise on structuring Firestore queries to avoid full-collection scans. -- Suggest using the Firebase Emulator Suite during development to eliminate billing costs. -- Guide on setting billing alerts and using the Firebase pricing calculator. - -### Scaling and Performance -- Advise on Firestore scaling patterns: avoid hotspots in sequential document IDs, use distributed counters for high-write fields. -- Design for Firestore's 1 write/second per document limit by sharding counters and using fan-out patterns. -- Recommend Cloud Functions concurrency and memory settings for optimal throughput. -- Guide on CDN caching strategies with Firebase Hosting for static and dynamic content. - -## Interaction Guidelines - -1. **Ask clarifying questions** before proposing an architecture: What are the query patterns? How many concurrent users? What is the expected data volume? -2. **Provide concrete examples** with TypeScript/JavaScript code snippets and Firestore security rules. -3. **Explain trade-offs** — every architectural decision has costs (monetary, complexity, latency). Make them explicit. -4. **Recommend the Firebase Emulator Suite** for local development and testing before any cloud deployment. -5. **Prioritize security** — always recommend locked-down security rules and validate all input in Cloud Functions. -6. **Reference official documentation** from firebase.google.com when appropriate. - -## Response Format - -When designing a Firestore schema, present it as a collection/document hierarchy: - -``` -├── users/{userId} -│ ├── name: string -│ ├── email: string -│ ├── createdAt: timestamp -│ └── roles: map { admin: bool, editor: bool } -│ -├── posts/{postId} -│ ├── title: string -│ ├── content: string -│ ├── authorId: string (ref → users) -│ ├── createdAt: timestamp -│ └── messages/{messageId} (subcollection) -│ ├── text: string -│ ├── senderId: string -│ └── createdAt: timestamp -``` - -When recommending Cloud Functions, include the trigger type, runtime configuration, and implementation skeleton: - -```typescript -// Firestore trigger — onCreate -export const onUserCreated = onDocumentCreated("users/{userId}", async (event) => { - const snapshot = event.data; - if (!snapshot) return; - const userData = snapshot.data(); - // Send welcome email, initialize user preferences, etc. -}); -``` - -When proposing security rules, include both the rule and a short explanation of why each condition exists. diff --git a/plugins/firebase/hooks/hooks.json b/plugins/firebase/hooks/hooks.json deleted file mode 100644 index 2beedc3..0000000 --- a/plugins/firebase/hooks/hooks.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "event": "on_save", - "description": "Validate Firestore security rules syntax on save", - "match": ["firestore.rules", "storage.rules", "**/*.rules"], - "command": "firebase emulators:exec --only firestore 'echo Rules compiled successfully' 2>&1 || echo 'Warning: Could not validate rules. Is the Firebase CLI installed?'" - }, - { - "event": "on_save", - "description": "Lint Cloud Functions TypeScript on save", - "match": ["functions/src/**/*.ts"], - "command": "cd functions && npx tsc --noEmit --pretty 2>&1 || true" - }, - { - "event": "on_commit", - "description": "Run Firestore security rules tests before commit", - "match": ["firestore.rules", "storage.rules"], - "command": "npm test --prefix functions 2>&1 || echo 'Warning: Security rules tests not configured.'" - }, - { - "event": "on_deploy", - "description": "Deploy Firebase services", - "command": "firebase deploy" - } - ] -} diff --git a/plugins/firebase/rules/firebase-security-rules.mdc b/plugins/firebase/rules/firebase-security-rules.mdc deleted file mode 100644 index a50b234..0000000 --- a/plugins/firebase/rules/firebase-security-rules.mdc +++ /dev/null @@ -1,190 +0,0 @@ ---- -description: Firebase security rules best practices for Firestore and Cloud Storage -globs: - - "firestore.rules" - - "storage.rules" - - "**/*.rules" -alwaysApply: false ---- - -# Firebase Security Rules - -## Authentication - -- Always authenticate users before allowing reads or writes. Never use `allow read, write: if true` in production. -- Use `request.auth != null` as the baseline condition for all rules. -- Check `request.auth.uid` to scope access to the document owner. - -```rules -// ✅ Correct — only authenticated users can read, only owners can write -match /users/{userId} { - allow read: if request.auth != null; - allow write: if request.auth.uid == userId; -} -``` - -## Data Validation - -- Validate incoming data types, required fields, and value ranges in write rules. -- Use `request.resource.data` to inspect the incoming document state. -- Use `resource.data` to inspect the existing document state (for updates). -- Ensure string lengths, number ranges, and enum values are within expected bounds. - -```rules -match /posts/{postId} { - allow create: if request.auth != null - && request.resource.data.title is string - && request.resource.data.title.size() > 0 - && request.resource.data.title.size() <= 200 - && request.resource.data.content is string - && request.resource.data.content.size() <= 50000 - && request.resource.data.createdAt == request.time; -} -``` - -## Custom Claims for Role-Based Access - -- Use Firebase Auth custom claims to implement role-based access control (RBAC). -- Set custom claims via the Admin SDK (Cloud Functions), never from the client. -- Access claims in rules via `request.auth.token`. - -```rules -// ✅ Correct — admin-only access via custom claims -match /admin/{document=**} { - allow read, write: if request.auth.token.admin == true; -} - -// ✅ Correct — role-based access -match /organizations/{orgId} { - allow read: if request.auth.token.orgId == orgId; - allow write: if request.auth.token.orgId == orgId - && request.auth.token.role in ["admin", "editor"]; -} -``` - -## Document Size Limits - -- Validate document size constraints to prevent abuse. -- Limit the number of fields and array lengths in incoming documents. -- Use `request.resource.data.keys().size()` to check field count. - -```rules -match /comments/{commentId} { - allow create: if request.auth != null - && request.resource.data.keys().size() <= 5 - && request.resource.data.text.size() <= 5000; -} -``` - -## Reusable Helper Functions - -- Extract common validation logic into helper functions to keep rules DRY. -- Define functions at the top of your rules file or within `match` blocks. - -```rules -rules_version = '2'; -service cloud.firestore { - // Helper: check if the user is authenticated - function isAuthenticated() { - return request.auth != null; - } - - // Helper: check if the user owns the document - function isOwner(userId) { - return request.auth.uid == userId; - } - - // Helper: check if the user has a specific role - function hasRole(role) { - return request.auth.token.role == role; - } - - // Helper: validate that a field is a non-empty string within a max length - function isValidString(field, maxLen) { - return field is string && field.size() > 0 && field.size() <= maxLen; - } - - match /databases/{database}/documents { - match /users/{userId} { - allow read: if isAuthenticated(); - allow write: if isOwner(userId); - } - - match /posts/{postId} { - allow read: if isAuthenticated(); - allow create: if isAuthenticated() - && isValidString(request.resource.data.title, 200) - && isValidString(request.resource.data.content, 50000); - allow update: if isOwner(resource.data.authorId); - allow delete: if isOwner(resource.data.authorId) || hasRole("admin"); - } - } -} -``` - -## Never Allow Unrestricted Access - -- Never deploy rules that grant unrestricted reads or writes. -- Use granular `read` sub-rules (`get`, `list`) and `write` sub-rules (`create`, `update`, `delete`) for fine-grained control. - -```rules -match /products/{productId} { - // ✅ Fine-grained — anyone can view, only admins can list all - allow get: if true; - allow list: if request.auth.token.admin == true; - - // ✅ Fine-grained — separate create, update, delete - allow create: if hasRole("editor"); - allow update: if hasRole("editor"); - allow delete: if hasRole("admin"); -} -``` - -## Cloud Storage Security Rules - -- Apply the same principles to Cloud Storage rules: authenticate, validate, and restrict. -- Validate file size, content type, and path structure. - -```rules -rules_version = '2'; -service firebase.storage { - match /b/{bucket}/o { - match /users/{userId}/avatar.{ext} { - allow read: if request.auth != null; - allow write: if request.auth.uid == userId - && request.resource.size < 5 * 1024 * 1024 // 5 MB limit - && request.resource.contentType.matches('image/.*'); - } - - match /public/{allPaths=**} { - allow read: if true; - allow write: if request.auth.token.admin == true; - } - } -} -``` - -## Testing Rules with the Emulator - -- Always test security rules locally using the Firebase Emulator Suite before deploying. -- Use `@firebase/rules-unit-testing` to write automated rule tests. -- Test both allowed and denied scenarios for every rule. - -```typescript -import { initializeTestEnvironment, assertSucceeds, assertFails } from "@firebase/rules-unit-testing"; - -const testEnv = await initializeTestEnvironment({ - projectId: "my-project", - firestore: { rules: fs.readFileSync("firestore.rules", "utf8") }, -}); - -// Test authenticated user can read their own document -const authedDb = testEnv.authenticatedContext("user123").firestore(); -await assertSucceeds(getDoc(doc(authedDb, "users", "user123"))); - -// Test unauthenticated user cannot read -const unauthedDb = testEnv.unauthenticatedContext().firestore(); -await assertFails(getDoc(doc(unauthedDb, "users", "user123"))); - -await testEnv.cleanup(); -``` diff --git a/plugins/firebase/rules/firestore.mdc b/plugins/firebase/rules/firestore.mdc deleted file mode 100644 index d69be6a..0000000 --- a/plugins/firebase/rules/firestore.mdc +++ /dev/null @@ -1,158 +0,0 @@ ---- -description: Firestore best practices for data modeling, queries, and performance -globs: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" - - "**/*.jsx" -alwaysApply: false ---- - -# Firestore Best Practices - -## Data Modeling - -- Design data models for query patterns, not for normalization — denormalize when needed to avoid expensive joins. -- Use subcollections for 1:N relationships where the child data is queried independently. -- Keep documents small; Firestore has a 1 MiB document size limit. Store large blobs in Cloud Storage instead. -- Prefer flat data structures; avoid deeply nested maps that complicate partial updates. -- Use collection group queries (`collectionGroup()`) for querying across subcollections with the same name. - -```typescript -// ✅ Correct — subcollection for 1:N relationship -const messagesRef = collection(db, "chats", chatId, "messages"); - -// ✅ Correct — collection group query across all "messages" subcollections -const allMessages = await getDocs( - query(collectionGroup(db, "messages"), where("senderId", "==", userId)) -); -``` - -## Batch Writes and Transactions - -- Use batch writes (`writeBatch()`) for multiple independent document updates (up to 500 operations per batch). -- Use transactions (`runTransaction()`) for atomic read-then-write operations where consistency is required. -- Never perform side effects (API calls, logging) inside transaction handlers — they may be retried. - -```typescript -// ✅ Correct — batch write for multiple independent updates -const batch = writeBatch(db); -batch.set(doc(db, "users", uid), userData); -batch.update(doc(db, "counters", "users"), { count: increment(1) }); -await batch.commit(); - -// ✅ Correct — transaction for atomic read-then-write -await runTransaction(db, async (transaction) => { - const docSnap = await transaction.get(docRef); - const newCount = (docSnap.data()?.count ?? 0) + 1; - transaction.update(docRef, { count: newCount }); -}); -``` - -## Indexes and Queries - -- Use composite indexes for queries that filter or order on multiple fields. Firestore will prompt you with an index creation link in error messages. -- Paginate large result sets with `limit()` and `startAfter()` using document snapshots or field values. -- Avoid using `!=` and `not-in` operators on large collections — they are less efficient. -- Use `orderBy()` in the same direction as your inequality filter field. - -```typescript -// ✅ Correct — paginated query -const firstPage = query( - collection(db, "products"), - orderBy("createdAt", "desc"), - limit(25) -); -const snapshot = await getDocs(firstPage); -const lastDoc = snapshot.docs[snapshot.docs.length - 1]; - -const nextPage = query( - collection(db, "products"), - orderBy("createdAt", "desc"), - startAfter(lastDoc), - limit(25) -); -``` - -## Timestamps and Server Values - -- Always use `serverTimestamp()` for `createdAt` and `updatedAt` fields instead of `new Date()` — this ensures consistency across clients regardless of clock skew. -- Use `increment()` for atomic counter updates instead of read-then-write patterns. - -```typescript -import { serverTimestamp, increment } from "firebase/firestore"; - -await setDoc(doc(db, "posts", postId), { - title: "Hello World", - createdAt: serverTimestamp(), - updatedAt: serverTimestamp(), - viewCount: 0, -}); - -// Atomic increment -await updateDoc(doc(db, "posts", postId), { - viewCount: increment(1), - updatedAt: serverTimestamp(), -}); -``` - -## Type Safety with Converters - -- Use Firestore data converters (`withConverter()`) to enforce TypeScript types on reads and writes. -- Define converter objects that implement `toFirestore()` and `fromFirestore()` methods. - -```typescript -interface Post { - title: string; - content: string; - createdAt: Timestamp; -} - -const postConverter: FirestoreDataConverter = { - toFirestore(post: Post): DocumentData { - return { title: post.title, content: post.content, createdAt: post.createdAt }; - }, - fromFirestore(snapshot: QueryDocumentSnapshot): Post { - const data = snapshot.data(); - return { title: data.title, content: data.content, createdAt: data.createdAt }; - }, -}; - -const postRef = doc(db, "posts", postId).withConverter(postConverter); -const postSnap = await getDoc(postRef); -const post: Post | undefined = postSnap.data(); // Typed! -``` - -## Offline Persistence - -- Enable offline persistence for web apps with `enableIndexedDbPersistence()` (or `enableMultiTabIndexedDbPersistence()` for multi-tab support). -- Handle the `FirestoreError` with code `failed-precondition` (multiple tabs) and `unimplemented` (browser lacks support). -- Use `onSnapshot()` listeners for real-time updates that work seamlessly offline. -- Be aware that offline writes are queued and applied when the client reconnects. - -```typescript -import { enableIndexedDbPersistence } from "firebase/firestore"; - -try { - await enableIndexedDbPersistence(db); -} catch (err: any) { - if (err.code === "failed-precondition") { - console.warn("Persistence requires single-tab access"); - } else if (err.code === "unimplemented") { - console.warn("Browser does not support persistence"); - } -} -``` - -## Security Rules - -- Never leave Firestore security rules in test mode (`allow read, write: if true`) in production. -- Always validate data types, required fields, and field value ranges in security rules. -- See the companion rule file `firebase-security-rules.mdc` for comprehensive security rule patterns. - -## Cost Optimization - -- Minimize document reads by caching and using `onSnapshot()` listeners (which count as one read per document change, not per listener attachment). -- Use `select()` field masks in Admin SDK queries to retrieve only the fields you need. -- Avoid full-collection reads; always scope queries with `where()` clauses. -- Use the Firestore emulator during development to avoid incurring billing costs. diff --git a/plugins/firebase/scripts/deploy-firebase.sh b/plugins/firebase/scripts/deploy-firebase.sh deleted file mode 100755 index 41a17a4..0000000 --- a/plugins/firebase/scripts/deploy-firebase.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# -# deploy-firebase.sh — Deploy Firebase services with optional target selection -# -# Usage: -# ./scripts/deploy-firebase.sh # Deploy all services -# ./scripts/deploy-firebase.sh functions # Deploy only Cloud Functions -# ./scripts/deploy-firebase.sh hosting # Deploy only Hosting -# ./scripts/deploy-firebase.sh firestore # Deploy Firestore rules + indexes -# ./scripts/deploy-firebase.sh functions hosting # Deploy multiple targets -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -cd "$PROJECT_ROOT" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*"; } - -# Check prerequisites -if ! command -v firebase &> /dev/null; then - log_error "Firebase CLI is not installed. Run: npm install -g firebase-tools" - exit 1 -fi - -if ! firebase projects:list &> /dev/null 2>&1; then - log_error "Not authenticated. Run: firebase login" - exit 1 -fi - -# Determine deployment targets -TARGETS=("$@") - -if [ ${#TARGETS[@]} -eq 0 ]; then - log_info "Deploying all Firebase services..." - firebase deploy -else - ONLY_FLAG=$(IFS=,; echo "${TARGETS[*]}") - log_info "Deploying: ${ONLY_FLAG}..." - - # Build Cloud Functions before deploying if functions is a target - for target in "${TARGETS[@]}"; do - if [ "$target" = "functions" ]; then - log_info "Building Cloud Functions..." - if [ -f "functions/package.json" ]; then - (cd functions && npm run build) - fi - break - fi - done - - firebase deploy --only "$ONLY_FLAG" -fi - -log_info "Deployment complete!" diff --git a/plugins/github/.cursor/plugin.json b/plugins/github/.cursor/plugin.json index 04931e0..c97eb17 100644 --- a/plugins/github/.cursor/plugin.json +++ b/plugins/github/.cursor/plugin.json @@ -1,7 +1,7 @@ { "name": "github", "version": "1.0.0", - "description": "Cursor plugin for GitHub — Actions, API, CLI, Pull Requests, and repository management", + "description": "Cursor plugin for GitHub \u2014 Actions, API, CLI, Pull Requests, and repository management", "author": { "name": "Cursor", "email": "plugins@cursor.com" @@ -9,13 +9,20 @@ "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["github", "git", "actions", "ci-cd", "pull-requests", "api"], + "keywords": [ + "github", + "git", + "actions", + "ci-cd", + "pull-requests", + "api" + ], "category": "developer-tools", - "tags": ["version-control", "ci-cd", "collaboration"], - "agents": "./agents/", + "tags": [ + "version-control", + "ci-cd", + "collaboration" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/github/CHANGELOG.md b/plugins/github/CHANGELOG.md index c08aa02..5f2859d 100644 --- a/plugins/github/CHANGELOG.md +++ b/plugins/github/CHANGELOG.md @@ -1,33 +1,13 @@ # Changelog -All notable changes to the GitHub plugin for Cursor will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + ## [1.0.0] - 2026-02-07 ### Added - -- **Plugin manifest** (`.cursor/plugin.json`) — Metadata, entry points, and configuration for the GitHub plugin. -- **Rules** - - `rules/github-actions.mdc` — Best practices for GitHub Actions workflow files: pinned action SHAs, least-privilege permissions, concurrency groups, dependency caching, matrix strategies, reusable workflows, and `pull_request_target` safety. - - `rules/github-api.mdc` — Best practices for GitHub API usage: Octokit SDK, rate-limit handling with retry logic, pagination, GraphQL for complex queries, webhook signature validation, and structured error handling. -- **Agents** - - `agents/github-actions-agent.md` — Specialized agent for creating CI/CD pipelines, debugging workflow failures, optimizing build times, configuring caching and matrix builds, and managing secrets and environments. - - `agents/github-pr-reviewer.md` — Agent for reviewing pull requests with structured findings across code quality, test coverage, CI status, breaking changes, documentation, and security, using severity levels (critical, high, medium, low, praise). -- **Skills** - - `skills/create-github-action/SKILL.md` — Step-by-step guide for creating composite and JavaScript/TypeScript GitHub Actions, including marketplace publishing, Dependabot setup, and common patterns. - - `skills/setup-ci-pipeline/SKILL.md` — CI/CD pipeline templates for Node.js, Python, Docker, and cloud deployments, with branch protection and Dependabot configuration. -- **Hooks** (`hooks/hooks.json`) - - Pre-commit hook to validate GitHub Actions workflow YAML syntax, check for pinned action SHAs, and verify permissions blocks. - - Post-save hook to auto-format workflow YAML files with Prettier. -- **MCP server** (`mcp.json`) — Configuration for `@modelcontextprotocol/server-github` providing AI-accessible GitHub API operations. -- **Scripts** - - `scripts/validate-workflows.sh` — Comprehensive workflow validation script checking YAML syntax, actionlint, pinned actions, permissions, concurrency, timeouts, `pull_request_target` safety, and hard-coded secrets. -- **Documentation** - - `README.md` — Plugin overview, setup guide, usage instructions, and project structure. - - `CHANGELOG.md` — This changelog. - - `LICENSE` — MIT License. - -[1.0.0]: https://github.com/cursor/service-plugin-generation/releases/tag/github-plugin-v1.0.0 +- Initial plugin with skills and MCP server configuration diff --git a/plugins/github/README.md b/plugins/github/README.md index dc3b0a1..7fb7bc0 100644 --- a/plugins/github/README.md +++ b/plugins/github/README.md @@ -1,170 +1,28 @@ -# GitHub Plugin for Cursor +# GitHub Plugin -A comprehensive Cursor plugin for working with GitHub — Actions, API, CLI, Pull Requests, and repository management. +Cursor plugin for GitHub — Actions, API, CLI, Pull Requests, and repository management. -## Overview - -This plugin provides rules, agents, skills, hooks, and MCP server configuration to help developers work effectively with GitHub's ecosystem directly from Cursor. - -## Contents - -### Rules - -| File | Scope | Description | -|------|-------|-------------| -| `rules/github-actions.mdc` | `.github/workflows/*.yml` | Best practices for GitHub Actions: pinned SHAs, least-privilege permissions, concurrency, caching, matrix builds, reusable workflows | -| `rules/github-api.mdc` | `**/*.ts`, `**/*.js` | Best practices for the GitHub API: Octokit SDK, rate limiting, pagination, GraphQL, webhook signatures, error handling | - -### Agents - -| File | Description | -|------|-------------| -| `agents/github-actions-agent.md` | Specialized agent for creating, debugging, and optimizing GitHub Actions workflows and CI/CD pipelines | -| `agents/github-pr-reviewer.md` | Agent for reviewing pull requests with structured findings, severity levels, and actionable suggestions | - -### Skills - -| Skill | Trigger | Description | -|-------|---------|-------------| -| `skills/create-github-action/SKILL.md` | Creating a custom GitHub Action | Step-by-step guide for composite and JavaScript actions, including marketplace publishing | -| `skills/setup-ci-pipeline/SKILL.md` | Setting up CI/CD | Pipeline patterns for Node.js, Python, Docker, and cloud deployments with branch protection and Dependabot | - -### Hooks - -| Hook | Event | Description | -|------|-------|-------------| -| `validate-github-actions-workflows` | `pre-commit` | Validates workflow YAML syntax, checks pinned actions, and verifies permissions blocks | -| `format-workflow-yaml` | `post-save` | Auto-formats workflow YAML with Prettier on save | - -### MCP Server - -The `mcp.json` configures the `@modelcontextprotocol/server-github` MCP server, providing AI-accessible GitHub API operations: - -- Repository search and management -- Issue and pull request CRUD -- Code search -- Branch management -- Workflow run inspection - -### Scripts - -| Script | Description | -|--------|-------------| -| `scripts/validate-workflows.sh` | Comprehensive workflow validator checking YAML syntax, pinned actions, permissions, concurrency, timeouts, and security | - -## Setup - -### 1. Install the Plugin - -Copy or symlink the `plugins/github/` directory into your Cursor workspace. - -### 2. Configure the MCP Server - -Set the `GITHUB_TOKEN` environment variable with a personal access token or GitHub App installation token: +## Installation ```bash -export GITHUB_TOKEN="ghp_your_token_here" +agent install github ``` -For fine-grained tokens, grant the minimum scopes your workflow requires: - -| Scope | Use Case | -|-------|----------| -| `repo` | Full repository access | -| `read:org` | Organization membership | -| `workflow` | GitHub Actions management | -| `read:packages` | Package registry access | - -### 3. Install Optional Tools +## Components -For the best experience, install these tools: - -```bash -# actionlint — GitHub Actions workflow linter -brew install actionlint # macOS -go install github.com/rhysd/actionlint/cmd/actionlint@latest # Go - -# yamllint — general YAML linter -pip install yamllint - -# gh — GitHub CLI -brew install gh # macOS -sudo apt install gh # Ubuntu/Debian -``` - -### 4. Validate Your Workflows - -```bash -chmod +x plugins/github/scripts/validate-workflows.sh -./plugins/github/scripts/validate-workflows.sh -``` - -## Usage - -### With Cursor AI - -The plugin activates automatically when you work with relevant files: - -- **Editing workflow YAML** — GitHub Actions rules and hooks activate, providing inline guidance and pre-commit validation. -- **Writing API code** — GitHub API rules activate when editing `.ts` or `.js` files that interact with the GitHub API. -- **Asking about CI/CD** — The GitHub Actions Agent provides expert guidance on pipeline creation and debugging. -- **Reviewing PRs** — The PR Reviewer Agent generates structured reviews with severity-tagged findings. - -### Running the Workflow Validator - -```bash -# Validate default directory (.github/workflows) -./plugins/github/scripts/validate-workflows.sh - -# Validate a custom directory -./plugins/github/scripts/validate-workflows.sh path/to/workflows -``` - -The validator checks: -1. YAML syntax validity -2. actionlint errors (if installed) -3. Actions pinned to full commit SHAs -4. Explicit `permissions:` blocks -5. Concurrency groups configured -6. Job `timeout-minutes` set -7. `pull_request_target` safety -8. No hard-coded secrets +### Skills -## Project Structure +| Skill | Description | +|:------|:------------| +| `create-github-action` | Step-by-step guide for creating composite and JavaScript GitHub Actions | +| `setup-ci-pipeline` | CI/CD pipeline patterns for Node.js, Python, Docker, and cloud deployments | -``` -plugins/github/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ ├── github-actions-agent.md # CI/CD specialist agent -│ └── github-pr-reviewer.md # PR review agent -├── rules/ -│ ├── github-actions.mdc # Actions best practices -│ └── github-api.mdc # API best practices -├── skills/ -│ ├── create-github-action/ -│ │ └── SKILL.md # Custom action creation guide -│ └── setup-ci-pipeline/ -│ └── SKILL.md # CI/CD pipeline setup guide -├── hooks/ -│ └── hooks.json # Pre-commit and post-save hooks -├── scripts/ -│ └── validate-workflows.sh # Workflow validation script -├── extensions/ # Extension point (reserved) -├── mcp.json # MCP server configuration -├── README.md # This file -├── CHANGELOG.md # Version history -└── LICENSE # MIT License -``` +### MCP Server -## Contributing +Provides access to the GitHub API via `@modelcontextprotocol/server-github`. -1. Fork the repository. -2. Create a feature branch. -3. Make your changes and add tests where applicable. -4. Submit a pull request with a clear description. +Requires `GITHUB_TOKEN` environment variable. ## License -MIT License — see [LICENSE](LICENSE) for details. +MIT diff --git a/plugins/github/agents/github-actions-agent.md b/plugins/github/agents/github-actions-agent.md deleted file mode 100644 index 99b7269..0000000 --- a/plugins/github/agents/github-actions-agent.md +++ /dev/null @@ -1,124 +0,0 @@ -# GitHub Actions Agent - -You are a specialized agent for GitHub Actions — the CI/CD and automation platform built into GitHub. Your purpose is to help developers write, debug, optimize, and maintain GitHub Actions workflows. - -## Identity - -- **Name**: GitHub Actions Agent -- **Expertise**: GitHub Actions workflows, CI/CD pipelines, YAML workflow syntax, runner environments, marketplace actions, and automation patterns. -- **Tone**: Concise, practical, and security-conscious. Provide working code first, then explain trade-offs. - -## Responsibilities - -### 1. Create CI/CD Pipelines - -Build complete, production-ready workflow files tailored to the project's language, framework, and deployment target. - -- Detect the project's stack from existing files (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `Dockerfile`, etc.). -- Produce workflows that lint, test, build, and deploy. -- Include appropriate triggers (`push`, `pull_request`, `release`, `schedule`, `workflow_dispatch`). -- Set correct `permissions` blocks and concurrency groups. - -### 2. Debug Workflow Failures - -Diagnose and fix failing workflows by analyzing error logs, runner context, and workflow configuration. - -- Read workflow run logs (via `gh run view --log-failed`) to isolate the failure step. -- Check for common issues: missing secrets, incorrect permissions, runner version mismatches, dependency conflicts, and timeout exhaustion. -- Suggest targeted fixes with minimal blast radius. - -### 3. Optimize Build Times - -Reduce CI duration and cost through caching, parallelism, and smarter step ordering. - -- Add or improve dependency caching (`actions/cache`, built-in caching in setup actions). -- Introduce matrix strategies to test across versions/platforms in parallel. -- Split monolithic jobs into parallel jobs with artifact passing. -- Identify and remove redundant steps (e.g., installing tools already present on the runner image). -- Recommend `paths` and `paths-ignore` filters to skip workflows for irrelevant changes. - -### 4. Set Up Caching - -Configure caching for all major ecosystems: - -| Ecosystem | Cache Path | Key Source | -|-----------|-------------------------------------|---------------------------------| -| Node.js | `~/.npm` or `node_modules` | `package-lock.json` | -| Python | `~/.cache/pip` | `requirements*.txt` | -| Go | `~/go/pkg/mod`, `~/.cache/go-build` | `go.sum` | -| Rust | `~/.cargo/registry`, `target/` | `Cargo.lock` | -| Java | `~/.gradle/caches` | `**/*.gradle*`, `gradle.properties` | -| Ruby | `vendor/bundle` | `Gemfile.lock` | - -### 5. Configure Matrix Builds - -Design matrix strategies that balance coverage and runner cost. - -- Choose meaningful axis combinations (OS, language version, dependency variant). -- Use `exclude` to skip impossible or redundant combinations. -- Use `include` to add one-off experimental entries with `continue-on-error`. -- Apply `fail-fast: false` for PR checks; `fail-fast: true` for gating deployments. - -### 6. Manage Secrets and Environments - -Advise on secure secret management and environment-based deployment gates. - -- Explain when to use repository secrets vs. environment secrets vs. organization secrets. -- Configure environments with required reviewers, wait timers, and branch protection rules. -- Set up OIDC authentication for cloud providers (AWS, GCP, Azure) to eliminate long-lived credentials. -- Guide users on rotating secrets and auditing secret access. - -## Process - -When a user asks for help: - -1. **Understand context** — Ask clarifying questions only when critical information is missing (language, deployment target, existing workflows). -2. **Propose a plan** — Briefly outline what the workflow will do before writing YAML. -3. **Write the workflow** — Produce a complete, copy-paste-ready `.yml` file following the rules in `github-actions.mdc`. -4. **Explain decisions** — After the code block, note any security considerations, cost implications, or trade-offs. -5. **Suggest next steps** — Recommend branch protection rules, status checks, or additional workflows the user might need. - -## Output Format - -When generating or modifying a workflow: - -```yaml -# .github/workflows/.yml -# -name: - -on: - # Triggers documented with comments - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - : - name: - runs-on: ubuntu-latest - timeout-minutes: - permissions: - contents: read - steps: - # Steps with pinned action SHAs and descriptive names -``` - -After the code block, include: - -- **Security notes** — Permissions granted, secrets required, supply-chain considerations. -- **Performance notes** — Expected run time, cache hit expectations, matrix size. -- **Maintenance notes** — How to update pinned SHAs (e.g., with Dependabot). - -## Constraints - -- Always pin third-party actions to full commit SHAs. -- Always set `permissions` to the minimum required. -- Always include `timeout-minutes` on every job. -- Never suggest `pull_request_target` with checkout of the PR HEAD. -- Prefer `ubuntu-latest` unless the user explicitly needs another OS. -- Use `actions/checkout`, `actions/setup-*`, and `actions/cache` from the official `actions` org for standard tasks. -- Recommend `workflow_dispatch` on all production workflows for manual re-runs. diff --git a/plugins/github/agents/github-pr-reviewer.md b/plugins/github/agents/github-pr-reviewer.md deleted file mode 100644 index eb5ef0d..0000000 --- a/plugins/github/agents/github-pr-reviewer.md +++ /dev/null @@ -1,144 +0,0 @@ -# GitHub Pull Request Reviewer Agent - -You are a specialized agent for reviewing GitHub pull requests. You analyze code changes, CI results, and PR metadata to provide thorough, actionable feedback that helps maintainers merge with confidence. - -## Identity - -- **Name**: GitHub PR Reviewer -- **Expertise**: Code review, software quality, testing practices, CI/CD status interpretation, security analysis, and API design review. -- **Tone**: Constructive and specific. Praise good patterns, flag risks clearly, and always suggest a concrete fix when raising an issue. - -## Review Checklist - -When reviewing a pull request, evaluate each of the following areas: - -### 1. Code Quality - -- **Readability**: Are names descriptive? Is the code self-documenting? -- **Complexity**: Are functions focused and short? Is nesting depth reasonable? -- **Duplication**: Is there copy-pasted logic that should be extracted? -- **Consistency**: Does the new code follow the project's existing style and conventions? -- **Error handling**: Are errors caught, logged, and surfaced appropriately? -- **Type safety**: Are types precise (avoid `any` in TypeScript, raw `Object` in Java)? -- **Dead code**: Are there commented-out blocks, unused imports, or unreachable branches? - -### 2. Test Coverage - -- **New code**: Do new functions and branches have corresponding tests? -- **Edge cases**: Are boundary conditions, empty inputs, and error paths tested? -- **Test quality**: Do tests assert behavior (not implementation details)? Are they deterministic? -- **Test naming**: Do test names describe the scenario and expected outcome? -- **Snapshot tests**: If snapshots changed, are the diffs intentional and reviewed? -- **Integration tests**: For API or database changes, are there integration tests? - -### 3. CI Status - -- **All checks passing**: Are required status checks green? -- **Flaky tests**: If a check failed and was re-run, investigate the flake. -- **New warnings**: Did the PR introduce new linter warnings or compiler warnings? -- **Build artifacts**: If the workflow produces artifacts (binaries, Docker images), verify they were built successfully. -- **Coverage delta**: If coverage reporting is configured, flag significant drops. - -### 4. Breaking Changes - -- **API surface**: Were any public function signatures, types, or exports changed? -- **Database schema**: Are there migrations? Are they reversible? -- **Configuration**: Were environment variables, config keys, or CLI flags added, renamed, or removed? -- **Dependencies**: Were major versions of dependencies bumped? Do they have breaking changes? -- **Protocol/wire format**: Were serialization formats (JSON schemas, protobuf, GraphQL) altered? - -### 5. Documentation Updates - -- **README**: If user-facing behavior changed, is the README updated? -- **Inline docs**: Are new public functions documented (JSDoc, docstrings, GoDoc)? -- **CHANGELOG**: Is there a changelog entry for user-visible changes? -- **Migration guide**: For breaking changes, is there a migration path documented? -- **API docs**: If REST/GraphQL endpoints changed, are OpenAPI specs or schema files updated? - -### 6. Security - -- **Secrets**: Are there hard-coded credentials, tokens, or API keys? -- **Input validation**: Is user input sanitized before use in queries, commands, or file paths? -- **Dependency vulnerabilities**: Do new dependencies have known CVEs? -- **Permissions**: Are file, network, or cloud permissions appropriately scoped? -- **Authentication/authorization**: Are access control checks present and correct? - -## Severity Levels - -Categorize every finding with one of the following severity levels: - -| Severity | Label | Meaning | Action Required | -|----------|-------|---------|-----------------| -| Critical | `🔴 critical` | Security vulnerability, data loss risk, or broken functionality | Must fix before merge | -| High | `🟠 high` | Bug, significant performance issue, or missing error handling | Should fix before merge | -| Medium | `🟡 medium` | Code quality concern, missing tests, or maintainability issue | Recommended to fix | -| Low | `🟢 low` | Style nit, minor improvement, or optional suggestion | Nice to have | -| Praise | `🔵 praise` | Excellent pattern, clever solution, or great documentation | No action — recognition | - -## Output Format - -Structure every review as follows: - -```markdown -## PR Review: - -### Summary -<2-3 sentence summary of what the PR does and overall assessment> - -### Verdict: - ---- - -### Findings - -#### 🔴 Critical: -**File**: `path/to/file.ts` L42-L58 -**Issue**: <Clear description of the problem> -**Impact**: <What could go wrong> -**Suggestion**: -\`\`\`diff -- <current code> -+ <suggested fix> -\`\`\` - -#### 🟡 Medium: <Title> -**File**: `path/to/file.ts` L10 -**Issue**: <Description> -**Suggestion**: <How to improve> - -#### 🔵 Praise: <Title> -**File**: `path/to/file.ts` L100-L120 -**Note**: <What was done well and why it's commendable> - ---- - -### Checklist -- [x] Code quality reviewed -- [x] Test coverage evaluated -- [x] CI status verified -- [ ] Breaking changes — N/A -- [x] Documentation checked -- [x] Security review done - -### Recommendations -- <Ordered list of suggested follow-up actions> -``` - -## Process - -1. **Read the PR description** — Understand intent, linked issues, and acceptance criteria. -2. **Check CI status** — Use `gh pr checks <number>` or the GitHub API to verify all checks passed. -3. **Review the diff** — Analyze changed files, focusing on logic changes over formatting. -4. **Evaluate tests** — Check that new and modified code has adequate test coverage. -5. **Assess impact** — Identify breaking changes, performance implications, and security risks. -6. **Compose findings** — Write each finding with a severity level, location, description, and suggestion. -7. **Summarize** — Provide an overall verdict with a brief rationale. - -## Constraints - -- Never approve a PR that has critical or high severity findings without them being addressed. -- Always include at least one praise item to encourage good practices. -- Keep suggestions actionable — include code diffs or specific steps, not vague advice. -- If unsure about a finding, label it clearly as a question rather than a demand. -- Respect the PR author's context — they may have constraints you are not aware of. -- When reviewing large PRs (>500 lines changed), suggest splitting into smaller PRs for future work. diff --git a/plugins/github/hooks/hooks.json b/plugins/github/hooks/hooks.json deleted file mode 100644 index 2e2f7a4..0000000 --- a/plugins/github/hooks/hooks.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "name": "validate-github-actions-workflows", - "event": "pre-commit", - "description": "Validates GitHub Actions workflow YAML syntax and common errors before committing", - "match": { - "files": [ - ".github/workflows/*.yml", - ".github/workflows/*.yaml" - ] - }, - "steps": [ - { - "name": "yaml-syntax-check", - "command": "python3 -c \"import yaml, sys; yaml.safe_load(open(sys.argv[1]))\" ${file}", - "description": "Verify the file is valid YAML", - "onFailure": "block" - }, - { - "name": "actionlint-check", - "command": "actionlint ${file}", - "description": "Run actionlint to check for GitHub Actions-specific errors", - "onFailure": "warn", - "condition": "command -v actionlint &> /dev/null" - }, - { - "name": "check-pinned-actions", - "command": "grep -n 'uses:' ${file} | grep -v '@[a-f0-9]\\{40\\}' | grep -v 'uses: ./' | head -20", - "description": "Warn about actions not pinned to a full SHA", - "onFailure": "warn" - }, - { - "name": "check-permissions", - "command": "grep -q 'permissions:' ${file}", - "description": "Verify that the workflow declares explicit permissions", - "onFailure": "warn" - } - ] - }, - { - "name": "format-workflow-yaml", - "event": "post-save", - "description": "Auto-formats GitHub Actions workflow YAML files on save for consistent style", - "match": { - "files": [ - ".github/workflows/*.yml", - ".github/workflows/*.yaml" - ] - }, - "steps": [ - { - "name": "prettier-yaml", - "command": "npx prettier --write --parser yaml ${file}", - "description": "Format YAML with Prettier for consistent indentation and quoting", - "onFailure": "ignore", - "condition": "command -v npx &> /dev/null" - } - ] - } - ] -} diff --git a/plugins/github/rules/github-actions.mdc b/plugins/github/rules/github-actions.mdc deleted file mode 100644 index ae84b22..0000000 --- a/plugins/github/rules/github-actions.mdc +++ /dev/null @@ -1,206 +0,0 @@ ---- -description: Best practices for writing and maintaining GitHub Actions workflow files -globs: - - .github/workflows/*.yml - - .github/workflows/*.yaml -alwaysApply: false ---- - -# GitHub Actions Best Practices - -## Pin Action Versions to Full-Length Commit SHAs - -Always pin third-party actions to a specific commit SHA rather than a mutable tag. Tags can be reassigned, creating a supply-chain risk. Using a SHA guarantees that the action code you reviewed is the code that runs. - -```yaml -# Bad — mutable tag can change without notice -- uses: actions/checkout@v4 - -# Good — pinned to an immutable commit SHA -- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 -``` - -Include a trailing comment with the human-readable version so maintainers can see the intent at a glance and tools like Dependabot can still propose updates. - -## Use Environment Variables for Secrets - -Never hard-code credentials, tokens, or sensitive values in workflow files. Always reference them through GitHub-encrypted secrets and surface them as environment variables. - -```yaml -env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - API_KEY: ${{ secrets.API_KEY }} - -steps: - - name: Deploy - run: ./deploy.sh - env: - DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} -``` - -Prefer scoping secrets to the narrowest context possible — step-level `env` rather than job-level or workflow-level when the value is only needed in one step. - -## Set Minimum Permissions with `permissions:` - -Apply the principle of least privilege. Declare the exact permissions a workflow or job needs and nothing more. Setting `permissions: {}` at the workflow level and overriding per-job prevents accidental token exposure. - -```yaml -permissions: {} # Default to no permissions - -jobs: - build: - permissions: - contents: read - packages: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 -``` - -Common permission scopes to consider: -- `contents: read` — clone the repo -- `pull-requests: write` — post comments or reviews -- `issues: write` — label or close issues -- `packages: write` — publish container images or packages -- `id-token: write` — OIDC-based cloud authentication (AWS, GCP, Azure) - -## Avoid `pull_request_target` with Checkout of PR Code - -The `pull_request_target` event runs in the context of the base branch and has access to secrets. If you check out the pull request HEAD inside such a workflow, untrusted code from a fork can exfiltrate secrets. - -```yaml -# Dangerous — grants PR code access to base-branch secrets -on: pull_request_target -steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - with: - ref: ${{ github.event.pull_request.head.sha }} - - run: npm test # Runs PR code with secrets available -``` - -If you must use `pull_request_target`, never check out the PR branch. Perform only metadata operations (labeling, commenting) or use a two-workflow approach where the untrusted build runs under `pull_request` and a separate privileged workflow consumes its artifacts. - -## Use Concurrency Groups to Prevent Redundant Runs - -Cancel in-progress runs when a newer commit is pushed to the same branch or PR, saving runner minutes and avoiding race conditions on deployments. - -```yaml -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -``` - -For production deployment workflows, you may want to let runs finish instead: - -```yaml -concurrency: - group: deploy-production - cancel-in-progress: false -``` - -## Cache Dependencies to Speed Up Builds - -Use `actions/cache` or the built-in caching of setup actions (e.g., `actions/setup-node`) to persist downloaded dependencies across runs. - -```yaml -- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: 20 - cache: npm - -# Or explicit cache step for full control -- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 - with: - path: | - ~/.npm - node_modules - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- -``` - -Cache paths to consider by ecosystem: -- **Node.js**: `~/.npm` or `node_modules` -- **Python**: `~/.cache/pip` -- **Go**: `~/go/pkg/mod` and `~/.cache/go-build` -- **Rust**: `~/.cargo/registry` and `target/` -- **Java/Gradle**: `~/.gradle/caches` - -## Use Matrix Strategies Efficiently - -Matrix builds let you test across multiple versions, OSes, or configurations in parallel. Keep matrices focused and use `exclude` or `include` to avoid combinatorial explosion. - -```yaml -strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [18, 20, 22] - exclude: - - os: windows-latest - node-version: 18 - include: - - os: ubuntu-latest - node-version: 22 - experimental: true -``` - -Set `fail-fast: false` when you want full coverage (e.g., in CI checks). Use `fail-fast: true` (default) when you only care about any failure, such as gating a deployment. - -Use `continue-on-error` on experimental matrix entries: - -```yaml -steps: - - run: npm test - continue-on-error: ${{ matrix.experimental || false }} -``` - -## Prefer Reusable Workflows and Composite Actions - -Avoid duplicating workflow logic. Extract shared patterns into reusable workflows (called via `workflow_call`) or composite actions. - -### Reusable Workflow - -```yaml -# .github/workflows/reusable-build.yml -on: - workflow_call: - inputs: - node-version: - required: true - type: string - secrets: - NPM_TOKEN: - required: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 - with: - node-version: ${{ inputs.node-version }} - - run: npm ci - - run: npm test -``` - -### Calling a Reusable Workflow - -```yaml -jobs: - test: - uses: ./.github/workflows/reusable-build.yml - with: - node-version: '20' - secrets: inherit -``` - -## Additional Guidelines - -- **Timeout**: Always set `timeout-minutes` on jobs to prevent stuck runners from consuming hours of billing. -- **Artifacts**: Use `actions/upload-artifact` and `actions/download-artifact` to pass data between jobs — never rely on shared file systems. -- **Conditional execution**: Use `if:` expressions to skip expensive steps when they are not needed (e.g., `if: github.event_name == 'push' && github.ref == 'refs/heads/main'`). -- **Workflow dispatch**: Add `workflow_dispatch` to production workflows so they can be triggered manually for debugging. -- **Status checks**: Mark required checks in branch protection rules to enforce that CI passes before merging. -- **Self-hosted runners**: If using self-hosted runners, keep them ephemeral (container-based or auto-scaling) and never store secrets on the runner file system. diff --git a/plugins/github/rules/github-api.mdc b/plugins/github/rules/github-api.mdc deleted file mode 100644 index 7ae8240..0000000 --- a/plugins/github/rules/github-api.mdc +++ /dev/null @@ -1,285 +0,0 @@ ---- -description: Best practices for interacting with the GitHub REST and GraphQL APIs -globs: - - "**/*.ts" - - "**/*.js" -alwaysApply: false ---- - -# GitHub API Best Practices - -## Use the Octokit SDK - -Prefer the official Octokit SDK over raw HTTP calls. Octokit handles authentication, pagination, media types, and error formatting, reducing boilerplate and surface area for bugs. - -```typescript -import { Octokit } from "@octokit/rest"; - -const octokit = new Octokit({ - auth: process.env.GITHUB_TOKEN, -}); - -// Typed, auto-completed API calls -const { data: repo } = await octokit.repos.get({ - owner: "octocat", - repo: "hello-world", -}); -``` - -For GitHub Apps, use the `@octokit/app` or `@octokit/auth-app` packages to manage installation tokens automatically: - -```typescript -import { App } from "@octokit/app"; - -const app = new App({ - appId: process.env.GITHUB_APP_ID!, - privateKey: process.env.GITHUB_APP_PRIVATE_KEY!, -}); - -const octokit = await app.getInstallationOctokit(installationId); -``` - -## Handle Rate Limiting with Retry Logic - -GitHub enforces rate limits (5,000 requests/hour for authenticated users, 15,000 for GitHub Apps). Always implement retry logic with exponential back-off that respects the `x-ratelimit-reset` and `retry-after` headers. - -Use the `@octokit/plugin-throttling` and `@octokit/plugin-retry` plugins: - -```typescript -import { Octokit } from "@octokit/rest"; -import { throttling } from "@octokit/plugin-throttling"; -import { retry } from "@octokit/plugin-retry"; - -const ThrottledOctokit = Octokit.plugin(throttling, retry); - -const octokit = new ThrottledOctokit({ - auth: process.env.GITHUB_TOKEN, - throttle: { - onRateLimit: (retryAfter: number, options: any, octokit: Octokit, retryCount: number) => { - octokit.log.warn(`Rate limit hit for ${options.method} ${options.url}`); - if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds`); - return true; - } - return false; - }, - onSecondaryRateLimit: (retryAfter: number, options: any, octokit: Octokit) => { - octokit.log.warn(`Secondary rate limit hit for ${options.method} ${options.url}`); - return true; - }, - }, - retry: { - doNotRetry: ["429"], - }, -}); -``` - -Always check rate-limit headers before intensive operations: - -```typescript -const { headers } = await octokit.rateLimit.get(); -const remaining = parseInt(headers["x-ratelimit-remaining"] ?? "0", 10); -const resetAt = new Date(parseInt(headers["x-ratelimit-reset"] ?? "0", 10) * 1000); - -if (remaining < 100) { - const waitMs = resetAt.getTime() - Date.now(); - console.warn(`Only ${remaining} requests remaining. Resets at ${resetAt.toISOString()}`); - if (waitMs > 0) await delay(waitMs); -} -``` - -## Paginate Results Properly - -Many GitHub API endpoints return paginated data. Never assume a single response contains all results. Use Octokit's built-in pagination helpers. - -```typescript -// Automatically fetches all pages -const allIssues = await octokit.paginate(octokit.issues.listForRepo, { - owner: "octocat", - repo: "hello-world", - state: "open", - per_page: 100, -}); - -// Or iterate page by page for memory efficiency -for await (const response of octokit.paginate.iterator(octokit.pulls.list, { - owner: "octocat", - repo: "hello-world", - state: "all", - per_page: 100, -})) { - for (const pr of response.data) { - await processPullRequest(pr); - } -} -``` - -When working with the REST API directly, follow `Link` headers: - -```typescript -function parseLinkHeader(header: string): Record<string, string> { - const links: Record<string, string> = {}; - for (const part of header.split(",")) { - const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/); - if (match) links[match[2]] = match[1]; - } - return links; -} -``` - -## Use the GraphQL API for Complex Queries - -When you need data from multiple related resources — such as a PR with its reviews, check runs, and changed files — a single GraphQL query is far more efficient than multiple REST calls. - -```typescript -const result = await octokit.graphql<PullRequestQueryResult>(` - query ($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - title - state - mergeable - additions - deletions - reviews(last: 10) { - nodes { - author { login } - state - body - } - } - commits(last: 1) { - nodes { - commit { - statusCheckRollup { - state - contexts(first: 50) { - nodes { - ... on CheckRun { - name - conclusion - } - } - } - } - } - } - } - files(first: 100) { - nodes { - path - additions - deletions - } - } - } - } - } -`, { - owner: "octocat", - repo: "hello-world", - number: 42, -}); -``` - -Observe the GraphQL rate limit: each query costs a calculated number of "points" based on the objects requested. Use `rateLimit` in queries to monitor: - -```graphql -query { - rateLimit { - limit - cost - remaining - resetAt - } -} -``` - -## Validate Webhook Signatures - -When receiving GitHub webhooks, always verify the `x-hub-signature-256` header to confirm the payload was sent by GitHub and has not been tampered with. - -```typescript -import { createHmac, timingSafeEqual } from "node:crypto"; - -function verifyWebhookSignature( - payload: string | Buffer, - signature: string, - secret: string, -): boolean { - const expected = "sha256=" + - createHmac("sha256", secret) - .update(payload) - .digest("hex"); - - if (expected.length !== signature.length) return false; - - return timingSafeEqual( - Buffer.from(expected), - Buffer.from(signature), - ); -} - -// Express middleware example -app.post("/webhooks/github", (req, res) => { - const signature = req.headers["x-hub-signature-256"] as string; - if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET!)) { - return res.status(401).send("Invalid signature"); - } - // Process event... - const event = req.headers["x-github-event"] as string; - handleEvent(event, JSON.parse(req.body)); - res.status(200).send("OK"); -}); -``` - -Use `timingSafeEqual` (not `===`) to prevent timing attacks. - -## Handle API Errors Gracefully - -GitHub returns structured error responses. Parse them properly and provide actionable messages to callers or end-users. - -```typescript -import { RequestError } from "@octokit/request-error"; - -async function getRepository(owner: string, repo: string) { - try { - const { data } = await octokit.repos.get({ owner, repo }); - return data; - } catch (error) { - if (error instanceof RequestError) { - switch (error.status) { - case 401: - throw new Error("Authentication failed. Check your GITHUB_TOKEN."); - case 403: - if (error.response?.headers["x-ratelimit-remaining"] === "0") { - const resetAt = new Date( - Number(error.response.headers["x-ratelimit-reset"]) * 1000, - ); - throw new Error(`Rate limit exceeded. Resets at ${resetAt.toISOString()}`); - } - throw new Error(`Forbidden: ${error.message}`); - case 404: - throw new Error(`Repository ${owner}/${repo} not found or you lack access.`); - case 422: - const validationErrors = (error.response?.data as any)?.errors ?? []; - throw new Error( - `Validation failed: ${validationErrors.map((e: any) => e.message).join(", ")}`, - ); - default: - throw new Error(`GitHub API error (${error.status}): ${error.message}`); - } - } - throw error; - } -} -``` - -## Additional Guidelines - -- **Conditional requests**: Use `If-None-Match` / `If-Modified-Since` headers (Octokit handles this via ETags) to avoid consuming rate-limit quota for unchanged data. -- **Preview headers**: Some API features require preview `Accept` headers. Check the GitHub docs and pass the appropriate media type. -- **Idempotency**: Design webhook handlers and API consumers to be idempotent — the same event may be delivered more than once. -- **Scoped tokens**: Use fine-grained personal access tokens or GitHub App installation tokens with the minimum required permissions. Never use classic PATs with broad scopes. -- **Audit logging**: Log all API calls (method, URL, status, rate-limit remaining) for debugging and compliance. -- **Testing**: Use `@octokit/fixtures` or mock responses in tests rather than calling the live API. For integration tests, create a dedicated test organization or use `gh api` with a test token. diff --git a/plugins/github/scripts/validate-workflows.sh b/plugins/github/scripts/validate-workflows.sh deleted file mode 100755 index 1cda1ff..0000000 --- a/plugins/github/scripts/validate-workflows.sh +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env bash -# -# validate-workflows.sh -# -# Validates GitHub Actions workflow files for syntax errors, best-practice -# violations, and common security issues. -# -# Usage: -# ./scripts/validate-workflows.sh [workflow-dir] -# -# Arguments: -# workflow-dir Path to the workflows directory (default: .github/workflows) -# -# Exit codes: -# 0 All checks passed -# 1 One or more checks failed -# -# Dependencies (optional but recommended): -# - actionlint (https://github.com/rhysd/actionlint) -# - yamllint (https://github.com/adrienverge/yamllint) -# - python3 (for YAML parsing fallback) - -set -euo pipefail - -# ─── Configuration ────────────────────────────────────────────────────────────── - -WORKFLOW_DIR="${1:-.github/workflows}" -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -ERRORS=0 -WARNINGS=0 - -# ─── Helper Functions ─────────────────────────────────────────────────────────── - -info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; } -success() { printf "${GREEN}[PASS]${NC} %s\n" "$*"; } -warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; WARNINGS=$((WARNINGS + 1)); } -fail() { printf "${RED}[FAIL]${NC} %s\n" "$*"; ERRORS=$((ERRORS + 1)); } - -command_exists() { command -v "$1" &> /dev/null; } - -# ─── Pre-flight Checks ───────────────────────────────────────────────────────── - -if [[ ! -d "$WORKFLOW_DIR" ]]; then - info "Workflow directory '$WORKFLOW_DIR' does not exist. Nothing to validate." - exit 0 -fi - -WORKFLOW_FILES=() -while IFS= read -r -d '' file; do - WORKFLOW_FILES+=("$file") -done < <(find "$WORKFLOW_DIR" -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) -print0 2>/dev/null) - -if [[ ${#WORKFLOW_FILES[@]} -eq 0 ]]; then - info "No workflow files found in '$WORKFLOW_DIR'. Nothing to validate." - exit 0 -fi - -info "Found ${#WORKFLOW_FILES[@]} workflow file(s) in '$WORKFLOW_DIR'" -echo "" - -# ─── Check 1: YAML Syntax ────────────────────────────────────────────────────── - -info "── Check 1: YAML Syntax ──" - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - - if command_exists yamllint; then - if yamllint -d relaxed "$file" > /dev/null 2>&1; then - success "$basename_file — valid YAML (yamllint)" - else - fail "$basename_file — invalid YAML" - yamllint -d relaxed "$file" 2>&1 | head -10 | sed 's/^/ /' - fi - elif command_exists python3; then - if python3 -c "import yaml, sys; yaml.safe_load(open('$file'))" 2>/dev/null; then - success "$basename_file — valid YAML (python3)" - else - fail "$basename_file — invalid YAML" - fi - else - warn "$basename_file — skipped (install yamllint or python3 for YAML validation)" - fi -done -echo "" - -# ─── Check 2: actionlint ─────────────────────────────────────────────────────── - -info "── Check 2: actionlint ──" - -if command_exists actionlint; then - for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - output=$(actionlint "$file" 2>&1) - if [[ -z "$output" ]]; then - success "$basename_file — no actionlint errors" - else - fail "$basename_file — actionlint found issues:" - echo "$output" | head -20 | sed 's/^/ /' - fi - done -else - warn "actionlint not installed — skipping (install: https://github.com/rhysd/actionlint)" -fi -echo "" - -# ─── Check 3: Pinned Action Versions ─────────────────────────────────────────── - -info "── Check 3: Pinned Action Versions ──" - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - unpinned=$(grep -n 'uses:' "$file" \ - | grep -v '@[a-f0-9]\{40\}' \ - | grep -v 'uses: \./' \ - | grep -v '^\s*#' \ - || true) - - if [[ -z "$unpinned" ]]; then - success "$basename_file — all actions pinned to SHAs" - else - warn "$basename_file — actions not pinned to full SHA:" - echo "$unpinned" | sed 's/^/ /' - fi -done -echo "" - -# ─── Check 4: Permissions Declared ───────────────────────────────────────────── - -info "── Check 4: Permissions Block ──" - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - if grep -q '^permissions:' "$file"; then - success "$basename_file — top-level permissions declared" - else - warn "$basename_file — no top-level 'permissions:' block (defaults to read-write)" - fi -done -echo "" - -# ─── Check 5: Concurrency Groups ─────────────────────────────────────────────── - -info "── Check 5: Concurrency Groups ──" - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - if grep -q '^concurrency:' "$file"; then - success "$basename_file — concurrency group configured" - else - warn "$basename_file — no 'concurrency:' block (duplicate runs may waste resources)" - fi -done -echo "" - -# ─── Check 6: Timeout Set ────────────────────────────────────────────────────── - -info "── Check 6: Job Timeouts ──" - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - if grep -q 'timeout-minutes:' "$file"; then - success "$basename_file — timeout-minutes configured" - else - warn "$basename_file — no 'timeout-minutes:' found (jobs default to 6 hours)" - fi -done -echo "" - -# ─── Check 7: pull_request_target Safety ──────────────────────────────────────── - -info "── Check 7: pull_request_target Safety ──" - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - if grep -q 'pull_request_target' "$file"; then - if grep -q 'actions/checkout' "$file" && grep -q 'pull_request.head' "$file"; then - fail "$basename_file — uses pull_request_target with checkout of PR HEAD (security risk!)" - else - warn "$basename_file — uses pull_request_target (review carefully for security)" - fi - else - success "$basename_file — no pull_request_target usage" - fi -done -echo "" - -# ─── Check 8: Secrets Not Hard-coded ─────────────────────────────────────────── - -info "── Check 8: Hard-coded Secrets ──" - -SECRET_PATTERNS='(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9_]{82}|gho_[a-zA-Z0-9]{36}|AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{48})' - -for file in "${WORKFLOW_FILES[@]}"; do - basename_file=$(basename "$file") - if grep -qE "$SECRET_PATTERNS" "$file"; then - fail "$basename_file — possible hard-coded secret detected!" - else - success "$basename_file — no hard-coded secrets found" - fi -done -echo "" - -# ─── Summary ──────────────────────────────────────────────────────────────────── - -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -if [[ $ERRORS -gt 0 ]]; then - printf "${RED}RESULT: %d error(s), %d warning(s)${NC}\n" "$ERRORS" "$WARNINGS" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - exit 1 -elif [[ $WARNINGS -gt 0 ]]; then - printf "${YELLOW}RESULT: 0 errors, %d warning(s)${NC}\n" "$WARNINGS" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - exit 0 -else - printf "${GREEN}RESULT: All checks passed!${NC}\n" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - exit 0 -fi diff --git a/plugins/launchdarkly/.cursor/plugin.json b/plugins/launchdarkly/.cursor/plugin.json index c344445..53cb22a 100644 --- a/plugins/launchdarkly/.cursor/plugin.json +++ b/plugins/launchdarkly/.cursor/plugin.json @@ -1,18 +1,27 @@ { "name": "launchdarkly", "version": "1.0.0", - "description": "Cursor plugin for LaunchDarkly — feature flags, experimentation, progressive rollouts, and targeting", - "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "description": "Cursor plugin for LaunchDarkly \u2014 feature flags, experimentation, progressive rollouts, and targeting", + "author": { + "name": "Cursor", + "email": "plugins@cursor.com" + }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["launchdarkly", "feature-flags", "experimentation", "rollouts", "targeting"], + "keywords": [ + "launchdarkly", + "feature-flags", + "experimentation", + "rollouts", + "targeting" + ], "category": "developer-tools", - "tags": ["feature-flags", "experimentation", "release-management"], - "agents": "./agents/", + "tags": [ + "feature-flags", + "experimentation", + "release-management" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/launchdarkly/CHANGELOG.md b/plugins/launchdarkly/CHANGELOG.md index 02f33cb..5f2859d 100644 --- a/plugins/launchdarkly/CHANGELOG.md +++ b/plugins/launchdarkly/CHANGELOG.md @@ -1,21 +1,13 @@ # Changelog -All notable changes to the LaunchDarkly Cursor Plugin will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-08 +## [Unreleased] -### Added +## [1.0.0] - 2026-02-07 -- **Plugin manifest** (`.cursor/plugin.json`) with full metadata and component references. -- **LaunchDarkly SDK rules** (`rules/launchdarkly-sdk.mdc`) — singleton client initialization, typed variation methods (`boolVariation`, `stringVariation`, `numberVariation`, `jsonVariation`), mandatory default values, context construction with stable keys, multi-context targeting, graceful client shutdown, initialization error handling, kebab-case flag key naming convention, avoiding flag dependencies and nesting, stale flag cleanup, offline/test mode with `TestData`, React SDK provider and hooks patterns, and SDK debugging. -- **Feature flag patterns rules** (`rules/launchdarkly-patterns.mdc`) — kill switches for critical features, progressive percentage-based rollouts with phased plans, targeting rules for beta users and segments, flag-driven trunk-based development, avoiding complex logic in flag evaluations, flag prerequisites, temporary vs. permanent flag classification, flag-based A/B testing and experimentation with `client.track()`, flag evaluation events for analytics with `variationDetail()`, feature flag lifecycle management, configuration flags for runtime settings, and server-side vs. client-side flag considerations. -- **Feature Flags Agent** (`agents/launchdarkly-flags-agent.md`) — AI strategist for flag strategy design, progressive rollout planning, targeting rules and segments, experimentation and A/B testing, stale flag cleanup, and SDK integration guidance across server-side and client-side applications. -- **Setup LaunchDarkly skill** (`skills/setup-launchdarkly/SKILL.md`) — end-to-end integration guide: SDK installation (server-side, client-side, Next.js), environment variable configuration, singleton client initialization with error handling, context factory for users, organizations, and anonymous contexts, Express/Fastify endpoint usage, React `LDProvider` setup, graceful shutdown handlers, `TestData` testing configuration, Next.js SSR integration with bootstrap hydration, and verification checklist. -- **Create Feature Flag skill** (`skills/create-feature-flag/SKILL.md`) — complete flag lifecycle: planning questionnaire, dashboard creation, CLI and REST API creation, server-side and client-side implementation, targeting rules configuration, progressive rollout execution with monitoring criteria and rollback procedures, multivariate flags for experiments, JSON configuration flags, testing flagged code paths, and post-rollout cleanup (archive, code removal, dead code elimination). -- **Pre-commit hooks** (`hooks/hooks.json`) — SDK key and API token leak detection, and variation call default value linting. -- **MCP server configuration** (`mcp.json`) — LaunchDarkly MCP server with tools for listing, creating, toggling, searching, and archiving flags, managing targeting rules, evaluating flags, listing segments and environments, and viewing audit logs. -- **Flag cleanup script** (`scripts/flag-cleanup.sh`) — multi-mode hygiene utility: `--check-secrets` for credential scanning, `--check-defaults` for missing default value warnings, `--find-stale` for flag reference discovery, and `--list-flags` for codebase flag inventory with text and JSON output. -- Project documentation: `README.md`, `CHANGELOG.md`, `LICENSE`. +### Added +- Initial plugin with skills and MCP server configuration diff --git a/plugins/launchdarkly/README.md b/plugins/launchdarkly/README.md index eb1c079..215ca33 100644 --- a/plugins/launchdarkly/README.md +++ b/plugins/launchdarkly/README.md @@ -1,108 +1,28 @@ -# LaunchDarkly Cursor Plugin +# LaunchDarkly Plugin -A comprehensive [Cursor](https://cursor.com) plugin for LaunchDarkly. Provides rules, agents, skills, hooks, and MCP server integration for feature flags, experimentation, progressive rollouts, and targeting. - -## Features - -| Component | Description | -|-----------|-------------| -| **Rules** | Best-practice rules for LaunchDarkly SDK usage and feature flag design patterns | -| **Agents** | Feature flag strategist agent for flag design, rollout planning, and cleanup | -| **Skills** | Step-by-step guides for LaunchDarkly setup and feature flag creation/management | -| **Hooks** | Pre-commit detection of hardcoded SDK keys and missing flag defaults | -| **MCP Server** | Direct LaunchDarkly flag management from Cursor via MCP server | -| **Scripts** | Flag hygiene utilities: secret scanning, stale flag detection, flag inventory | +Cursor plugin for LaunchDarkly — feature flags, experimentation, progressive rollouts, and targeting. ## Installation -Copy or symlink this plugin directory into your project's `.cursor/plugins/` folder: - ```bash -cp -r plugins/launchdarkly /your-project/.cursor/plugins/launchdarkly +agent install launchdarkly ``` -Or reference it in your Cursor plugin configuration. - ## Components -### Rules - -- **`launchdarkly-sdk.mdc`** — SDK best practices: singleton client initialization, typed variation methods, default values, context construction, multi-contexts, graceful shutdown, initialization error handling, flag key naming conventions (kebab-case), avoiding flag dependencies, stale flag cleanup, offline/test mode, React SDK patterns, and debugging. -- **`launchdarkly-patterns.mdc`** — Feature flag design patterns: kill switches for critical features, progressive percentage-based rollouts, targeting rules for beta users, flag-driven trunk-based development, avoiding complex flag logic, flag prerequisites, temporary vs. permanent flags, A/B testing and experimentation, flag evaluation events for analytics, flag lifecycle management, configuration flags, and server-side vs. client-side flag considerations. - -### Agents - -- **`launchdarkly-flags-agent.md`** — AI feature flag strategist that helps design flag strategies, plan progressive rollouts, configure targeting rules and segments, set up A/B tests and experiments, identify and clean up stale flags, and integrate LaunchDarkly SDKs across server-side and client-side applications. - ### Skills -- **`setup-launchdarkly`** — End-to-end LaunchDarkly integration: SDK installation, environment variable configuration, singleton client initialization, context factory, Express/Fastify usage, React provider setup, shutdown handlers, test data source configuration, Next.js SSR integration, and verification checklist. -- **`create-feature-flag`** — Complete feature flag lifecycle: planning, dashboard/CLI/API creation, code implementation (server-side and client-side), targeting rules, progressive rollout execution with monitoring criteria, rollback procedures, multivariate flags for experiments, JSON configuration flags, testing flagged code, and post-rollout cleanup. - -### Hooks - -- **Pre-commit SDK key check** — Scans staged files for hardcoded LaunchDarkly SDK keys, API access tokens, and mobile keys before every commit. -- **Pre-commit default value lint** — Warns when LaunchDarkly variation calls appear to be missing explicit default values. +| Skill | Description | +|:------|:------------| +| `setup-launchdarkly` | SDK integration, client initialization, context setup, React provider, and testing | +| `create-feature-flag` | Flag lifecycle from creation through rollout, experimentation, and cleanup | ### MCP Server -- **LaunchDarkly MCP Server** (`@launchdarkly/mcp-server`) — Tools to list, create, toggle, and search feature flags, manage targeting rules, evaluate flags, list segments and environments, view audit logs, and archive flags directly from Cursor. - -### Scripts - -- **`flag-cleanup.sh`** — Multi-purpose flag hygiene script with four modes: - - `--check-secrets` — Scan for hardcoded LaunchDarkly SDK keys, API tokens, and mobile keys - - `--check-defaults` — Warn about variation calls missing explicit default values - - `--find-stale` — Find and list all feature flag references in the codebase with file locations - - `--list-flags` — List all LaunchDarkly flag keys referenced in code (text or JSON output) - -## Project Structure - -``` -plugins/launchdarkly/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── launchdarkly-flags-agent.md # Feature flag strategist agent -├── rules/ -│ ├── launchdarkly-sdk.mdc # SDK best practices -│ └── launchdarkly-patterns.mdc # Feature flag design patterns -├── skills/ -│ ├── setup-launchdarkly/ -│ │ └── SKILL.md # LaunchDarkly integration guide -│ └── create-feature-flag/ -│ └── SKILL.md # Feature flag lifecycle guide -├── hooks/ -│ └── hooks.json # Pre-commit hook definitions -├── scripts/ -│ └── flag-cleanup.sh # Flag hygiene utilities -├── extensions/ -├── mcp.json # MCP server configuration -├── README.md -├── CHANGELOG.md -└── LICENSE -``` - -## Configuration - -### LaunchDarkly Credentials - -The plugin expects LaunchDarkly credentials to be configured via environment variables: - -| Variable | Description | -|----------|-------------| -| `LAUNCHDARKLY_SDK_KEY` | Server-side SDK key (secret — never expose in client-side code) | -| `LAUNCHDARKLY_CLIENT_SIDE_ID` | Client-side environment ID (safe to expose in browser apps) | -| `LAUNCHDARKLY_ACCESS_TOKEN` | API access token for the LaunchDarkly REST API and MCP server | -| `LAUNCHDARKLY_PROJECT_KEY` | Project key (default: `default`) | -| `LAUNCHDARKLY_ENVIRONMENT_KEY` | Environment key (default: `production`) | - -### Getting Your Credentials +Provides LaunchDarkly flag management via `@launchdarkly/mcp-server`. -1. **SDK Key**: LaunchDarkly dashboard → Account Settings → Projects → Your Project → Environments → SDK key -2. **Client-Side ID**: Same location as SDK key — look for "Client-side ID" -3. **API Access Token**: LaunchDarkly dashboard → Account Settings → Authorization → Access tokens → Create token +Requires `LAUNCHDARKLY_ACCESS_TOKEN` environment variable. ## License -MIT — see [LICENSE](./LICENSE) for details. +MIT diff --git a/plugins/launchdarkly/agents/launchdarkly-flags-agent.md b/plugins/launchdarkly/agents/launchdarkly-flags-agent.md deleted file mode 100644 index fa6e037..0000000 --- a/plugins/launchdarkly/agents/launchdarkly-flags-agent.md +++ /dev/null @@ -1,147 +0,0 @@ -# LaunchDarkly Feature Flags Agent - -## Identity - -You are a LaunchDarkly feature flag strategist and implementation agent. You help developers design, implement, manage, and clean up feature flags using LaunchDarkly. You are deeply familiar with the LaunchDarkly platform, SDKs (server-side and client-side), experimentation, targeting, and release management best practices. - -## Expertise - -- Feature flag architecture and design patterns -- LaunchDarkly SDK integration (Node.js, React, Python, Go, Java, and more) -- Progressive rollout strategies and canary deployments -- Targeting rules, user segments, and multi-context targeting -- A/B testing and experimentation with LaunchDarkly -- Flag lifecycle management and stale flag cleanup -- Trunk-based development with feature flags -- Kill switches and operational safety controls -- LaunchDarkly API and automation workflows -- Flag dependencies, prerequisites, and mutual exclusion groups -- Relay Proxy configuration for high-availability deployments -- Data Export and analytics integration - -## Responsibilities - -### 1. Flag Strategy Design - -Help developers decide what to flag and how: - -- Determine whether a flag should be **temporary** (feature rollout) or **permanent** (operational control). -- Recommend flag types: boolean, string, number, or JSON based on the use case. -- Design flag naming conventions consistent with the project (kebab-case, feature-prefixed). -- Plan flag variations — two-way toggles for simple features, multivariate for experiments. -- Identify when flags are overkill (trivial config changes, one-time migrations). - -### 2. Progressive Rollout Planning - -Create phased rollout plans tailored to risk and scale: - -- Define rollout phases: canary (1–2%) → early adopters (5–10%) → partial (25%) → majority (50%) → GA (100%). -- Recommend monitoring criteria for each phase (error rates, latency, conversion, support tickets). -- Set up automatic rollback triggers when error thresholds are exceeded. -- Design rollout schedules aligned with business hours and team availability. -- Plan rollout strategies for backend, frontend, and mobile simultaneously. - -### 3. Targeting Rules and Segments - -Help build precise targeting configurations: - -- Design context schemas with the right attributes for targeting (`plan`, `role`, `region`, `deviceType`). -- Create reusable segments: internal users, beta testers, enterprise accounts, geographic cohorts. -- Build complex targeting rules combining user attributes, organization attributes, and percentage rollouts. -- Recommend multi-context setups for applications that need to target by user, organization, and device simultaneously. -- Test targeting rules by evaluating flags against sample contexts. - -### 4. Experimentation and A/B Testing - -Guide developers through rigorous A/B testing: - -- Define the hypothesis, primary metric, secondary metrics, and success criteria before starting. -- Design experiment flag variations (control + treatment(s)). -- Calculate required sample size and experiment duration for statistical significance. -- Set up `track()` calls for conversion events and metric values. -- Analyze experiment results: interpret confidence intervals, statistical significance, and practical significance. -- Recommend next steps: ship the winner, iterate, or abandon. - -### 5. Stale Flag Cleanup - -Systematically identify and remove technical debt from old flags: - -- Query the LaunchDarkly API for flags that have been 100% rolled out for more than N days. -- Cross-reference flag keys with code references to find dead code paths. -- Generate cleanup plans: which flags to archive, which code branches to remove. -- Produce pull request descriptions for flag removal changes. -- Update tests that reference removed flags. - -### 6. SDK Integration - -Help integrate LaunchDarkly SDKs correctly: - -- Set up singleton client initialization patterns for server-side applications. -- Configure React providers and hooks for client-side applications. -- Wire up context construction from authentication and session data. -- Implement graceful shutdown and event flushing. -- Set up test data sources for unit and integration testing. -- Configure the Relay Proxy for high-throughput or air-gapped environments. - -## Behavior Guidelines - -- Always ask about the team's deployment frequency, risk tolerance, and monitoring capabilities before recommending a rollout strategy. -- Distinguish between temporary and permanent flags in every recommendation. -- Warn about common anti-patterns: nested flag checks, using flags for authorization, flag sprawl, and missing defaults. -- Recommend measurable success criteria for every experiment and rollout. -- When reviewing existing flags, identify stale flags and suggest cleanup steps. -- Reference LaunchDarkly documentation and best practices when explaining concepts. -- Provide code examples in the language and framework the developer is using. -- Consider the full stack — if a flag affects both backend and frontend, address both. - -## Example Interactions - -### Designing a Flag Strategy - -**User:** We're launching a new pricing page. How should we flag it? - -**Agent approach:** -1. Create a temporary boolean flag `pricing-page-v2` for the rollout. -2. Create a permanent kill switch `pricing-page-kill-switch` that defaults to on. -3. Plan a progressive rollout: 5% internal → 10% beta → 25% → 50% → 100%. -4. Set up targeting: 100% for employees (email domain), 100% for beta segment. -5. Define success metrics: bounce rate, plan upgrade conversion, support ticket volume. -6. Set a cleanup deadline: remove `pricing-page-v2` two weeks after reaching 100%. - -### Planning an A/B Test - -**User:** We want to test two different checkout button designs. - -**Agent approach:** -1. Create a multivariate string flag `checkout-button-experiment` with three variations: `"control"`, `"green-cta"`, `"animated-cta"`. -2. Define the hypothesis: "A green CTA button will increase checkout conversion by ≥5%." -3. Set the primary metric: `checkout-completed` event tracked via `client.track()`. -4. Calculate sample size based on current conversion rate and minimum detectable effect. -5. Configure equal traffic split (33/33/34) across variations. -6. Run for the calculated duration — do not peek or stop early. -7. Analyze results and recommend shipping, iterating, or abandoning. - -### Cleaning Up Stale Flags - -**User:** We have over 200 flags and many seem unused. Help me clean up. - -**Agent approach:** -1. Use the LaunchDarkly API to list all flags with their `lastRequested` timestamp and rollout status. -2. Identify flags that are 100% rolled out and haven't changed in 30+ days. -3. Cross-reference with code references to confirm the flag is still in the codebase. -4. Categorize: ready to remove, needs investigation, permanent (keep). -5. Generate a prioritized cleanup backlog with estimated effort per flag. -6. Produce code diff summaries for the top 10 cleanup candidates. - -### Integrating the SDK - -**User:** I'm setting up LaunchDarkly in a Next.js app with server-side rendering. - -**Agent approach:** -1. Install `@launchdarkly/node-server-sdk` for server-side and `launchdarkly-react-client-sdk` for the client. -2. Create a singleton server client initialized in a module-level variable. -3. In `getServerSideProps`, evaluate flags and pass them as props. -4. On the client, wrap the app with `<LDProvider>` using the client-side ID. -5. Use `useFlags()` hook in components for real-time flag updates. -6. Set up context construction from the session/auth data on both server and client. -7. Handle hydration mismatch by bootstrapping the client SDK with server-evaluated values. diff --git a/plugins/launchdarkly/hooks/hooks.json b/plugins/launchdarkly/hooks/hooks.json deleted file mode 100644 index 13f792d..0000000 --- a/plugins/launchdarkly/hooks/hooks.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "name": "check-launchdarkly-sdk-key", - "description": "Prevent committing hardcoded LaunchDarkly SDK keys, API access tokens, and mobile keys", - "event": "pre-commit", - "command": "bash ./scripts/flag-cleanup.sh --check-secrets", - "filePattern": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json", "**/*.yaml", "**/*.yml", "**/*.env", "**/*.env.*"], - "excludePattern": ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/coverage/**"], - "enabled": true, - "failOnError": true - }, - { - "name": "lint-flag-defaults", - "description": "Warn when LaunchDarkly variation calls are missing explicit default values", - "event": "pre-commit", - "command": "bash ./scripts/flag-cleanup.sh --check-defaults", - "filePattern": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], - "excludePattern": ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/test/**", "**/__tests__/**"], - "enabled": true, - "failOnError": false - } - ] -} diff --git a/plugins/launchdarkly/rules/launchdarkly-patterns.mdc b/plugins/launchdarkly/rules/launchdarkly-patterns.mdc deleted file mode 100644 index 2f07766..0000000 --- a/plugins/launchdarkly/rules/launchdarkly-patterns.mdc +++ /dev/null @@ -1,299 +0,0 @@ ---- -description: Feature flag design patterns and strategies for LaunchDarkly -globs: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" -alwaysApply: false ---- - -# LaunchDarkly Feature Flag Patterns - -## Kill Switches for Critical Features - -- Wrap every critical or risky feature behind a boolean kill switch flag. -- Kill switches should default to **on** (the feature works) and can be turned **off** instantly to disable. -- Name kill switches clearly: `payments-kill-switch`, `search-circuit-breaker`. -- Kill switches are permanent flags — they stay in the codebase and dashboard indefinitely. -- Document the blast radius and rollback procedure for each kill switch. - -```typescript -// ✅ Correct — kill switch pattern (default true = feature enabled) -const paymentsEnabled = await client.boolVariation("payments-kill-switch", context, true); - -if (!paymentsEnabled) { - return res.status(503).json({ - error: "Payment processing is temporarily unavailable", - retryAfter: 300, - }); -} - -// Proceed with payment processing -await processPayment(order); -``` - -## Progressive Rollouts (Percentage-Based) - -- Use percentage-based rollouts to gradually expose a feature to increasing traffic. -- Start at 1–5%, monitor error rates and performance, then increase incrementally. -- Use LaunchDarkly's percentage rollout targeting (not custom random logic). -- Define a rollout plan before starting: 5% → 10% → 25% → 50% → 100%. -- Always roll out by a sticky attribute (user key) so users get a consistent experience. - -```typescript -// The percentage rollout is configured in the LaunchDarkly dashboard. -// In code, simply evaluate the flag — LaunchDarkly handles bucketing. -const useNewCheckout = await client.boolVariation("checkout-v2-rollout", context, false); - -if (useNewCheckout) { - return renderNewCheckout(cart); -} else { - return renderLegacyCheckout(cart); -} -``` - -### Recommended Rollout Plan - -| Phase | Percentage | Duration | Monitoring | -|-------|-----------|----------|------------| -| Canary | 1–2% | 1 hour | Error rate, latency p99, support tickets | -| Early adopters | 5–10% | 24 hours | Conversion rate, error rate, user feedback | -| Partial | 25% | 48 hours | Business KPIs, performance dashboards | -| Majority | 50% | 48 hours | Full metric suite | -| General availability | 100% | Permanent | Standard monitoring | - -## Targeting Rules for Beta Users - -- Use targeting rules to enable features for specific user segments before broader rollout. -- Create segments in LaunchDarkly for internal users, beta testers, enterprise customers, etc. -- Use context attributes for targeting: `plan`, `company`, `email domain`, `role`. -- Layer targeting with percentage rollouts: "100% for beta segment, 10% for everyone else." - -```typescript -// Context with targeting attributes -const context: LaunchDarkly.LDContext = { - kind: "multi", - user: { - key: user.id, - email: user.email, - custom: { - plan: user.plan, // "free" | "pro" | "enterprise" - role: user.role, // "admin" | "member" - betaTester: user.isBeta, // true | false - }, - }, - organization: { - key: org.id, - name: org.name, - custom: { - plan: org.plan, - industry: org.industry, - }, - }, -}; - -// LaunchDarkly targeting rules (configured in dashboard): -// Rule 1: If user.betaTester = true → serve true -// Rule 2: If organization.plan = "enterprise" → serve true -// Rule 3: Default → percentage rollout 10% -const showAdvancedAnalytics = await client.boolVariation( - "analytics-advanced-dashboard", - context, - false -); -``` - -## Flag-Driven Trunk-Based Development - -- Use feature flags to merge incomplete features into the main branch safely. -- Wrap work-in-progress code behind a flag that is **off** in production. -- This eliminates long-lived feature branches, reduces merge conflicts, and enables CI/CD. -- Mark these flags as **temporary** — they must be removed after the feature is complete and fully rolled out. - -```typescript -// ✅ Correct — trunk-based development with feature flag -const enableNewSearchEngine = await client.boolVariation( - "search-new-engine", - context, - false // off by default — safe to merge to main -); - -export async function search(query: string): Promise<SearchResults> { - if (enableNewSearchEngine) { - return newElasticsearchEngine.search(query); - } - return legacySearchEngine.search(query); -} -``` - -## Avoid Complex Logic in Flag Evaluations - -- Keep flag evaluation code simple — a single `if/else` or switch statement. -- Do not embed business logic, computations, or side effects inside flag branches. -- Extract complex behavior into separate functions and use the flag to select which function to call. - -```typescript -// ❌ Wrong — complex logic mixed with flag evaluation -const useNewPricing = await client.boolVariation("pricing-v2", context, false); -if (useNewPricing) { - const discount = user.plan === "enterprise" ? 0.2 : user.tenure > 12 ? 0.1 : 0; - const tax = calculateTax(order.total, user.country); - order.total = order.total * (1 - discount) + tax; - // ... 50 more lines -} - -// ✅ Correct — flag selects a strategy, logic lives in dedicated functions -const useNewPricing = await client.boolVariation("pricing-v2", context, false); -const pricingStrategy = useNewPricing ? newPricingStrategy : legacyPricingStrategy; -order.total = await pricingStrategy.calculate(order, user); -``` - -## Use Flag Prerequisites - -- Use LaunchDarkly's prerequisite feature to model flag dependencies explicitly. -- A prerequisite ensures flag B is only evaluated if flag A is serving a specific variation. -- This replaces ad-hoc nested flag checks in code. -- Configure prerequisites in the LaunchDarkly dashboard, not in application code. - -``` -Dashboard configuration: - Flag: "checkout-express-pay" - Prerequisite: "checkout-v2" must serve "true" - - → express-pay is automatically OFF unless checkout-v2 is ON. -``` - -```typescript -// ✅ Correct — prerequisites configured in dashboard; code is simple -const expressPayEnabled = await client.boolVariation("checkout-express-pay", context, false); -if (expressPayEnabled) { - renderExpressPayButton(); -} -// No need to check "checkout-v2" in code — the prerequisite handles it. -``` - -## Temporary vs. Permanent Flags - -- **Temporary flags** gate in-progress features and rollouts. Remove them after full rollout. -- **Permanent flags** are operational controls: kill switches, entitlements, configuration. -- Mark every flag as temporary or permanent when creating it in the dashboard. -- Set up alerts or scheduled reviews for temporary flags older than 30 days. - -| Type | Examples | Lifecycle | -|------|----------|-----------| -| Temporary | `checkout-v2-rollout`, `search-new-algorithm` | Create → rollout → remove | -| Permanent | `payments-kill-switch`, `max-upload-size-mb` | Create → maintain indefinitely | - -```typescript -// Temporary flag — remove after full rollout -const showNewOnboarding = await client.boolVariation("onboarding-v2", context, false); - -// Permanent flag — operational control, stays forever -const maintenanceMode = await client.boolVariation("maintenance-mode", context, false); -``` - -## Flag-Based A/B Testing and Experimentation - -- Use LaunchDarkly's experimentation features to run A/B tests with statistical rigor. -- Define a hypothesis, success metric, and sample size before starting an experiment. -- Use multivariate flags for experiments with more than two variants. -- Track conversion events using `client.track()` to feed the experimentation engine. -- Let the experiment run to statistical significance — do not peek and make decisions early. - -```typescript -// Multivariate experiment — three checkout button styles -const checkoutButtonVariant = await client.stringVariation( - "checkout-button-experiment", - context, - "control" // "control" | "variant-a" | "variant-b" -); - -// Render the appropriate variant -switch (checkoutButtonVariant) { - case "variant-a": - renderGreenButton(); - break; - case "variant-b": - renderAnimatedButton(); - break; - default: - renderDefaultButton(); -} - -// Track the conversion event for the experiment -function onCheckoutComplete(orderId: string, revenue: number): void { - client.track("checkout-completed", context, { orderId }, revenue); -} -``` - -## Handle Flag Evaluation Events for Analytics - -- Use `client.track()` to send custom events tied to flag evaluations. -- Track meaningful business events: purchases, sign-ups, page views, errors. -- Include metric values (numeric) for revenue, duration, or count-based metrics. -- Use `client.variationDetail()` to get the evaluation reason alongside the value for debugging. - -```typescript -// Track a business event with a metric value -client.track("purchase-completed", context, { orderId: "abc-123" }, 49.99); - -// Track a count-based event -client.track("search-performed", context, { query }); - -// Get detailed evaluation information for debugging -const detail = await client.variationDetail("checkout-v2", context, false); -console.log({ - value: detail.value, - variationIndex: detail.variationIndex, - reason: detail.reason, // e.g., { kind: "RULE_MATCH", ruleIndex: 0 } -}); -``` - -## Feature Flag Lifecycle Management - -- Every flag should follow a defined lifecycle: **plan → create → test → rollout → clean up**. -- Document the purpose, owner, and expected removal date for every temporary flag. -- Use LaunchDarkly tags to categorize flags by team, project, or lifecycle stage. -- Automate stale flag detection using the LaunchDarkly API or code references. - -### Lifecycle Stages - -``` -Plan → Create → Dev/Test → Canary → Rollout → Monitor → GA → Remove (temporary) - → Maintain (permanent) -``` - -## Configuration Flags - -- Use feature flags for runtime configuration: rate limits, timeouts, UI copy, theme colors. -- These are typically permanent, multivariate flags (string, number, or JSON). -- Changing configuration via flags is instant — no redeployment needed. - -```typescript -// Runtime configuration via flags -const rateLimitPerMinute = await client.numberVariation("api-rate-limit", context, 100); -const supportEmail = await client.stringVariation("support-email", context, "help@example.com"); -const featureConfig = await client.jsonVariation("dashboard-config", context, { - maxWidgets: 10, - refreshInterval: 30, - theme: "light", -}); -``` - -## Server-Side vs. Client-Side Flags - -- Use server-side SDKs for backend services — they receive the full flag ruleset. -- Use client-side SDKs for frontend apps — they only receive evaluated flag values for the current context. -- Never expose your server-side SDK key in client-side code. -- Use the client-side ID (safe to expose) in browser and mobile applications. -- Keep sensitive flags server-side only — mark them as unavailable to client-side SDKs in the dashboard. - -```typescript -// ✅ Server-side — uses SDK key (secret, backend only) -import * as LaunchDarkly from "@launchdarkly/node-server-sdk"; -const serverClient = LaunchDarkly.init(process.env.LD_SDK_KEY!); - -// ✅ Client-side — uses client-side ID (safe to expose) -import { LDProvider } from "launchdarkly-react-client-sdk"; -<LDProvider clientSideID="your-client-side-id">...</LDProvider> -``` diff --git a/plugins/launchdarkly/rules/launchdarkly-sdk.mdc b/plugins/launchdarkly/rules/launchdarkly-sdk.mdc deleted file mode 100644 index 56e4f8c..0000000 --- a/plugins/launchdarkly/rules/launchdarkly-sdk.mdc +++ /dev/null @@ -1,278 +0,0 @@ ---- -description: LaunchDarkly SDK best practices for TypeScript and JavaScript projects -globs: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" - - "**/*.jsx" -alwaysApply: false ---- - -# LaunchDarkly SDK Best Practices - -## Singleton Client Initialization - -- Initialize the LaunchDarkly client **once** at application startup and reuse it everywhere. -- Never create a new client per request, per component render, or per function invocation. -- Store the client instance in a module-level singleton or dependency injection container. - -```typescript -// ✅ Correct — singleton pattern -import * as LaunchDarkly from "@launchdarkly/node-server-sdk"; - -let ldClient: LaunchDarkly.LDClient | null = null; - -export async function getLDClient(): Promise<LaunchDarkly.LDClient> { - if (!ldClient) { - ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!); - await ldClient.waitForInitialization({ timeout: 5 }); - } - return ldClient; -} -``` - -```typescript -// ❌ Wrong — new client per request -app.get("/api/feature", async (req, res) => { - const client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!); - await client.waitForInitialization(); - const value = await client.variation("my-flag", context, false); - res.json({ value }); -}); -``` - -## Typed Variation Methods - -- Use the typed variation methods (`boolVariation`, `stringVariation`, `numberVariation`, `jsonVariation`) instead of the generic `variation` method when your SDK version supports them. -- Typed methods provide better type safety and make flag value expectations explicit. - -```typescript -// ✅ Correct — typed variation methods -const showBanner = await client.boolVariation("show-promo-banner", context, false); -const bannerColor = await client.stringVariation("banner-color", context, "blue"); -const maxRetries = await client.numberVariation("max-retries", context, 3); -const uiConfig = await client.jsonVariation("ui-config", context, { layout: "default" }); -``` - -```typescript -// ❌ Avoid — untyped variation -const showBanner = await client.variation("show-promo-banner", context, false); -``` - -## Always Provide Default Values - -- Every `variation` or typed variation call **must** include a meaningful default value as the last argument. -- The default is returned when the client is offline, the flag does not exist, or evaluation fails. -- Choose defaults that represent the **safe, pre-existing behavior** (e.g., feature off, conservative limit). - -```typescript -// ✅ Correct — safe default -const rateLimit = await client.numberVariation("api-rate-limit", context, 100); - -// ❌ Wrong — no thought given to default -const rateLimit = await client.numberVariation("api-rate-limit", context, 0); -``` - -## Context Construction - -- Always identify users/contexts with a unique, stable `key`. -- Include attributes that are useful for targeting (e.g., `email`, `plan`, `country`). -- Use `anonymous: true` for unauthenticated users to avoid polluting your user base. -- Never use PII as the context key — use an opaque ID and pass PII as attributes. - -```typescript -// ✅ Correct — well-formed context -const context: LaunchDarkly.LDContext = { - kind: "user", - key: user.id, - name: user.displayName, - email: user.email, - custom: { - plan: user.plan, - company: user.companyId, - country: user.country, - }, -}; -``` - -## Multi-Contexts for Complex Targeting - -- Use multi-contexts when targeting involves more than one entity (e.g., user + organization + device). -- Each context kind has its own `key` and attributes. -- Multi-contexts enable targeting rules like "enable for users in organization X on mobile devices." - -```typescript -// ✅ Correct — multi-context -const context: LaunchDarkly.LDContext = { - kind: "multi", - user: { - key: user.id, - name: user.name, - email: user.email, - }, - organization: { - key: org.id, - name: org.name, - custom: { plan: org.plan, industry: org.industry }, - }, - device: { - key: deviceId, - custom: { platform: "ios", appVersion: "3.2.1" }, - }, -}; -``` - -## Graceful Client Shutdown - -- Always close the LaunchDarkly client when the application shuts down. -- This flushes pending analytics events and releases resources. -- Register shutdown handlers for `SIGTERM` and `SIGINT`. - -```typescript -async function shutdown(): Promise<void> { - const client = await getLDClient(); - await client.close(); - process.exit(0); -} - -process.on("SIGTERM", shutdown); -process.on("SIGINT", shutdown); -``` - -## Handle Initialization Errors - -- Wrap `waitForInitialization()` in a try-catch and handle timeouts. -- Decide whether your application can start in degraded mode (serving defaults) or should fail fast. -- Log initialization failures with enough detail for debugging (but never log the SDK key). - -```typescript -try { - const client = LaunchDarkly.init(sdkKey); - await client.waitForInitialization({ timeout: 5 }); - console.log("LaunchDarkly client initialized successfully"); -} catch (error) { - console.error("LaunchDarkly initialization failed:", error); - // Option A: Fail fast - process.exit(1); - // Option B: Continue with defaults (degraded mode) - // All variation calls will return default values -} -``` - -## Flag Key Naming Convention - -- Use **kebab-case** for all flag keys: `enable-dark-mode`, `max-upload-size`, `checkout-v2`. -- Prefix flags with the feature area or team: `payments-enable-stripe-v2`, `search-use-new-algorithm`. -- Never use camelCase, snake_case, or spaces in flag keys. -- Keep keys concise but descriptive. - -```typescript -// ✅ Correct — kebab-case with feature prefix -await client.boolVariation("checkout-enable-express-pay", context, false); -await client.numberVariation("search-max-results", context, 50); - -// ❌ Wrong — inconsistent naming -await client.boolVariation("enableExpressPay", context, false); -await client.boolVariation("search_max_results", context, 50); -``` - -## Avoid Flag Dependencies and Nesting - -- Do not nest flag evaluations (checking one flag inside another flag's branch). -- Flag dependencies create hidden coupling and make rollouts unpredictable. -- If you need complex logic, use a single multivariate flag or LaunchDarkly's prerequisite feature. - -```typescript -// ❌ Wrong — nested flag checks -const enableV2 = await client.boolVariation("checkout-v2", context, false); -if (enableV2) { - const enableExpress = await client.boolVariation("checkout-v2-express", context, false); - // ... -} - -// ✅ Correct — single multivariate flag or use prerequisites in the dashboard -const checkoutMode = await client.stringVariation("checkout-mode", context, "v1"); -// "v1" | "v2-standard" | "v2-express" -``` - -## Clean Up Stale Flags - -- Remove flags from code after they have been fully rolled out or deprecated. -- Stale flags add code complexity, confuse developers, and create evaluation overhead. -- Mark flags as temporary or permanent when creating them in the LaunchDarkly dashboard. -- Audit flags quarterly — if a flag has been 100% rolled out for more than 2 sprints, remove it. -- Use LaunchDarkly's code references feature to track where flags are used in your codebase. - -```typescript -// ❌ Stale — flag has been 100% on for 6 months -const enabled = await client.boolVariation("new-homepage", context, false); -if (enabled) { - renderNewHomepage(); -} else { - renderOldHomepage(); // Dead code -} - -// ✅ Cleaned up — old code path removed -renderNewHomepage(); -``` - -## Use Offline Mode for Testing - -- Use the LaunchDarkly test data source or offline mode in unit tests and local development. -- Never connect to the production LaunchDarkly environment from test suites. -- Use `TestData` to set flag values deterministically in tests. - -```typescript -import * as LaunchDarkly from "@launchdarkly/node-server-sdk"; -import { TestData } from "@launchdarkly/node-server-sdk/integrations"; - -// ✅ Correct — test data source -const td = TestData(); -td.update(td.flag("my-flag").booleanFlag().variationForAll(true)); - -const testClient = LaunchDarkly.init("fake-sdk-key", { - updateProcessor: td, -}); -await testClient.waitForInitialization(); - -// Now all evaluations of "my-flag" return true -const result = await testClient.boolVariation("my-flag", context, false); -expect(result).toBe(true); -``` - -## React SDK Patterns - -- Use the `LDProvider` at the app root and `useFlags` / `useLDClient` hooks in components. -- Never call `variation` directly in React components — use the hook-based API. -- Wrap flag-dependent components in `Suspense` or handle the loading state from the provider. - -```tsx -// ✅ Correct — React provider and hooks -import { LDProvider, useFlags } from "launchdarkly-react-client-sdk"; - -function App() { - return ( - <LDProvider clientSideID={process.env.REACT_APP_LD_CLIENT_ID!}> - <FeatureBanner /> - </LDProvider> - ); -} - -function FeatureBanner() { - const { showPromoBanner } = useFlags(); - if (!showPromoBanner) return null; - return <Banner text="Special offer!" />; -} -``` - -## Debugging - -- Enable the SDK's built-in logger during development to see flag evaluation details. -- Log the flag key, context key, and returned value for debugging targeting issues. -- Use LaunchDarkly's live flag evaluation debugger in the dashboard. - -```typescript -const client = LaunchDarkly.init(sdkKey, { - logger: LaunchDarkly.basicLogger({ level: "debug" }), -}); -``` diff --git a/plugins/launchdarkly/scripts/flag-cleanup.sh b/plugins/launchdarkly/scripts/flag-cleanup.sh deleted file mode 100755 index a24ff1f..0000000 --- a/plugins/launchdarkly/scripts/flag-cleanup.sh +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env bash -# -# flag-cleanup.sh -# LaunchDarkly feature flag hygiene utilities. -# -# Usage: -# ./scripts/flag-cleanup.sh --check-secrets Check for hardcoded SDK keys in staged files -# ./scripts/flag-cleanup.sh --check-defaults Warn about variation calls missing defaults -# ./scripts/flag-cleanup.sh --find-stale Find potentially stale flag references in code -# ./scripts/flag-cleanup.sh --list-flags List all flag keys referenced in the codebase -# -# Examples: -# ./scripts/flag-cleanup.sh --check-secrets -# ./scripts/flag-cleanup.sh --find-stale --days 30 -# ./scripts/flag-cleanup.sh --list-flags --format json -# - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -MODE="${1:---help}" -DAYS="${DAYS:-30}" -FORMAT="${FORMAT:-text}" - -# Parse additional flags -shift || true -while [[ $# -gt 0 ]]; do - case "$1" in - --days) DAYS="$2"; shift ;; - --format) FORMAT="$2"; shift ;; - *) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;; - esac - shift -done - -# ─── Check for hardcoded SDK keys ───────────────────────────────────────────── - -check_secrets() { - echo -e "${BLUE}Checking for hardcoded LaunchDarkly credentials...${NC}" - - local exit_code=0 - local staged_files - - # Get staged files (if in a git repo with staged changes) - if git rev-parse --git-dir > /dev/null 2>&1; then - staged_files=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true) - else - # Not in git or no staged files — scan all source files - staged_files=$(find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.env" \) \ - -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" -not -path "*/build/*" 2>/dev/null || true) - fi - - if [[ -z "$staged_files" ]]; then - echo -e " ${GREEN}✓${NC} No files to check" - return 0 - fi - - # Pattern: LaunchDarkly SDK keys (sdk-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) - local sdk_key_pattern='sdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' - - # Pattern: LaunchDarkly API access tokens (api-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) - local api_token_pattern='api-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' - - # Pattern: LaunchDarkly mobile keys (mob-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) - local mobile_key_pattern='mob-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' - - while IFS= read -r file; do - [[ -z "$file" ]] && continue - [[ ! -f "$file" ]] && continue - - # Skip binary files - if file --mime "$file" 2>/dev/null | grep -q "binary"; then - continue - fi - - # Check for SDK keys - if grep -Pn "$sdk_key_pattern" "$file" 2>/dev/null; then - echo -e " ${RED}✗ Hardcoded SDK key found in: ${file}${NC}" - exit_code=1 - fi - - # Check for API tokens - if grep -Pn "$api_token_pattern" "$file" 2>/dev/null; then - echo -e " ${RED}✗ Hardcoded API access token found in: ${file}${NC}" - exit_code=1 - fi - - # Check for mobile keys - if grep -Pn "$mobile_key_pattern" "$file" 2>/dev/null; then - echo -e " ${RED}✗ Hardcoded mobile key found in: ${file}${NC}" - exit_code=1 - fi - done <<< "$staged_files" - - if [[ $exit_code -eq 0 ]]; then - echo -e " ${GREEN}✓${NC} No hardcoded LaunchDarkly credentials found" - else - echo "" - echo -e "${RED}ERROR: Hardcoded LaunchDarkly credentials detected.${NC}" - echo -e "Use environment variables instead:" - echo -e " ${YELLOW}LAUNCHDARKLY_SDK_KEY${NC} for server-side SDK keys" - echo -e " ${YELLOW}LAUNCHDARKLY_ACCESS_TOKEN${NC} for API access tokens" - echo -e " ${YELLOW}LAUNCHDARKLY_MOBILE_KEY${NC} for mobile SDK keys" - fi - - return $exit_code -} - -# ─── Check for missing default values ──────────────────────────────────────── - -check_defaults() { - echo -e "${BLUE}Checking for LaunchDarkly variation calls without explicit defaults...${NC}" - - local warnings=0 - - # Find variation calls — look for calls with only 2 arguments (missing default) - # Pattern: .variation("key", context) without a third argument - # This is a heuristic — it may produce false positives - local files - files=$(find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) \ - -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" -not -path "*/build/*" \ - -not -path "*/test/*" -not -path "*/__tests__/*" 2>/dev/null || true) - - while IFS= read -r file; do - [[ -z "$file" ]] && continue - [[ ! -f "$file" ]] && continue - - # Look for .variation( with only two arguments before the closing paren - # This is approximate — complex expressions may not be caught - if grep -Pn '\.variation\s*\(\s*"[^"]+"\s*,\s*\w+\s*\)' "$file" 2>/dev/null; then - echo -e " ${YELLOW}⚠ Possible missing default value in: ${file}${NC}" - ((warnings++)) || true - fi - done <<< "$files" - - if [[ $warnings -eq 0 ]]; then - echo -e " ${GREEN}✓${NC} All variation calls appear to have default values" - else - echo "" - echo -e "${YELLOW}WARNING: ${warnings} file(s) may have variation calls without explicit defaults.${NC}" - echo -e "Always provide a default value: client.variation(\"key\", context, ${YELLOW}defaultValue${NC})" - fi - - return 0 -} - -# ─── Find stale flag references ────────────────────────────────────────────── - -find_stale() { - echo -e "${BLUE}Scanning for feature flag references in codebase...${NC}" - echo "" - - # Find all variation calls and extract flag keys - local flag_keys - flag_keys=$(grep -rPoh '(?:boolVariation|stringVariation|numberVariation|jsonVariation|variation)\s*\(\s*"([^"]+)"' \ - --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \ - . 2>/dev/null | \ - grep -oP '"[^"]+"' | tr -d '"' | sort -u || true) - - if [[ -z "$flag_keys" ]]; then - echo -e " ${GREEN}✓${NC} No feature flag references found in the codebase" - return 0 - fi - - local count - count=$(echo "$flag_keys" | wc -l) - echo -e " Found ${YELLOW}${count}${NC} unique flag key(s) referenced in code:" - echo "" - - while IFS= read -r key; do - [[ -z "$key" ]] && continue - - # Count references - local refs - refs=$(grep -r "$key" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | wc -l || echo 0) - - # List files - local files - files=$(grep -rl "$key" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | head -5 || true) - - echo -e " ${BLUE}${key}${NC} — ${refs} reference(s)" - while IFS= read -r f; do - [[ -z "$f" ]] && continue - echo -e " → ${f}" - done <<< "$files" - echo "" - done <<< "$flag_keys" - - echo -e "${YELLOW}Review these flags in the LaunchDarkly dashboard to check if any are fully rolled out and ready for cleanup.${NC}" -} - -# ─── List all flag keys ────────────────────────────────────────────────────── - -list_flags() { - echo -e "${BLUE}Listing all LaunchDarkly flag keys in the codebase...${NC}" - echo "" - - local flag_keys - flag_keys=$(grep -rPoh '(?:boolVariation|stringVariation|numberVariation|jsonVariation|variation)\s*\(\s*"([^"]+)"' \ - --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \ - . 2>/dev/null | \ - grep -oP '"[^"]+"' | tr -d '"' | sort -u || true) - - if [[ -z "$flag_keys" ]]; then - echo "No flag keys found." - return 0 - fi - - if [[ "$FORMAT" == "json" ]]; then - echo "[" - local first=true - while IFS= read -r key; do - [[ -z "$key" ]] && continue - if [[ "$first" == true ]]; then - echo " \"${key}\"" - first=false - else - echo ", \"${key}\"" - fi - done <<< "$flag_keys" - echo "]" - else - while IFS= read -r key; do - [[ -z "$key" ]] && continue - echo " • ${key}" - done <<< "$flag_keys" - fi -} - -# ─── Help ───────────────────────────────────────────────────────────────────── - -show_help() { - echo -e "${BLUE}LaunchDarkly Flag Cleanup Utilities${NC}" - echo "" - echo "Usage: ./scripts/flag-cleanup.sh <command> [options]" - echo "" - echo "Commands:" - echo " --check-secrets Check for hardcoded LaunchDarkly SDK keys in staged/source files" - echo " --check-defaults Warn about variation calls missing explicit default values" - echo " --find-stale Find and list all feature flag references in the codebase" - echo " --list-flags List all LaunchDarkly flag keys referenced in the codebase" - echo " --help Show this help message" - echo "" - echo "Options:" - echo " --days N Number of days for staleness threshold (default: 30)" - echo " --format FORMAT Output format: text or json (default: text)" - echo "" - echo "Examples:" - echo " ./scripts/flag-cleanup.sh --check-secrets" - echo " ./scripts/flag-cleanup.sh --find-stale" - echo " ./scripts/flag-cleanup.sh --list-flags --format json" -} - -# ─── Main ───────────────────────────────────────────────────────────────────── - -case "$MODE" in - --check-secrets) check_secrets ;; - --check-defaults) check_defaults ;; - --find-stale) find_stale ;; - --list-flags) list_flags ;; - --help) show_help ;; - *) echo -e "${RED}Unknown command: ${MODE}${NC}"; show_help; exit 1 ;; -esac diff --git a/plugins/mongodb/.cursor/plugin.json b/plugins/mongodb/.cursor/plugin.json index a681f25..058e9dc 100644 --- a/plugins/mongodb/.cursor/plugin.json +++ b/plugins/mongodb/.cursor/plugin.json @@ -1,18 +1,27 @@ { "name": "mongodb", "version": "1.0.0", - "description": "Cursor plugin for MongoDB — schema design, queries, aggregation, indexes, and Mongoose ODM", - "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "description": "Cursor plugin for MongoDB \u2014 schema design, queries, aggregation, indexes, and Mongoose ODM", + "author": { + "name": "Cursor", + "email": "plugins@cursor.com" + }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["mongodb", "mongoose", "nosql", "database", "aggregation"], + "keywords": [ + "mongodb", + "mongoose", + "nosql", + "database", + "aggregation" + ], "category": "backend", - "tags": ["database", "nosql", "odm"], - "agents": "./agents/", + "tags": [ + "database", + "nosql", + "odm" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/mongodb/CHANGELOG.md b/plugins/mongodb/CHANGELOG.md index 6f21739..5f2859d 100644 --- a/plugins/mongodb/CHANGELOG.md +++ b/plugins/mongodb/CHANGELOG.md @@ -1,20 +1,13 @@ # Changelog -All notable changes to the MongoDB Cursor Plugin will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-08 +## [Unreleased] -### Added +## [1.0.0] - 2026-02-07 -- Initial release of the MongoDB Cursor Plugin. -- `mongodb-schema.mdc` rule for MongoDB schema design best practices (embedding vs referencing, data types, JSON Schema validation, discriminators, schema versioning, Subset/Bucket patterns, sharding, field naming, TTL indexes, capped collections). -- `mongodb-queries.mdc` rule for MongoDB query best practices (compound indexes with ESR rule, projections, aggregation pipelines with `$match`/`$lookup`/`$group`, cursor-based pagination, `bulkWrite`, duplicate key error handling, transactions with retry logic, change streams with resume tokens, `explain()` analysis, read/write concerns, native driver vs Mongoose guidance, connection management, error handling). -- MongoDB Schema Design Agent for document modeling, embedding vs referencing decisions, index strategies, aggregation pipeline optimization, sharding, change streams, Mongoose ODM guidance, and performance analysis. -- Setup MongoDB skill with step-by-step Node.js/TypeScript integration for both native driver and Mongoose, Docker Compose setup, connection singleton patterns, model definitions, and database seeding. -- Design Schema skill covering entity/access pattern analysis, embedding vs referencing decision framework, design pattern application (Subset, Bucket, Computed, Extended Reference), index creation, `explain()` validation, and JSON Schema collection validation. -- File-change hooks for model/schema file modifications, migration files, and Docker Compose changes. -- MCP server configuration using `mongodb-mcp-server` for database introspection and query execution. -- `mongodb-health.sh` script for connection checks, server info, database stats, collection details, replica set status, and index usage statistics. +### Added +- Initial plugin with skills and MCP server configuration diff --git a/plugins/mongodb/README.md b/plugins/mongodb/README.md index c085ffd..9a99218 100644 --- a/plugins/mongodb/README.md +++ b/plugins/mongodb/README.md @@ -1,112 +1,28 @@ -# MongoDB Cursor Plugin +# MongoDB Plugin -A comprehensive Cursor plugin for [MongoDB](https://www.mongodb.com/) that helps developers design document schemas, write efficient queries, build aggregation pipelines, manage indexes, and work with both the native MongoDB driver and Mongoose ODM. +Cursor plugin for MongoDB — schema design, queries, aggregation, indexes, and Mongoose ODM. -## Features +## Installation -- **Schema Design Rules** — Best practices for MongoDB document modeling, embedding vs referencing, data types, JSON Schema validation, discriminators, schema versioning, sharding, and field naming conventions. -- **Query Best Practices Rules** — Guidelines for indexing, projections, aggregation pipelines, cursor-based pagination, bulkWrite, duplicate key handling, transactions, change streams, explain() analysis, read/write concerns, and connection management. -- **Schema Design Agent** — An AI agent specialized in document schema design, embedding vs referencing decisions, index strategies (ESR rule), aggregation pipeline optimization, and Mongoose-specific guidance. -- **Setup Skill** — Step-by-step guide for connecting a Node.js/TypeScript application to MongoDB with both the native driver and Mongoose ODM. -- **Schema Design Skill** — Complete workflow for designing MongoDB schemas, choosing data patterns, creating indexes, and validating design decisions. -- **File-Change Hooks** — Notifications when model files, migrations, or Docker Compose files are modified. -- **MCP Server Integration** — Database introspection, querying, and aggregation via the MongoDB MCP server. -- **Health Check Script** — CLI script for checking MongoDB connectivity, server status, database stats, and index usage. - -## Structure - -``` -plugins/mongodb/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── mongodb-schema-agent.md # Schema design AI agent -├── rules/ -│ ├── mongodb-schema.mdc # Schema design best practices -│ └── mongodb-queries.mdc # Query and performance best practices -├── skills/ -│ ├── setup-mongodb/ -│ │ └── SKILL.md # MongoDB + Node.js setup guide -│ └── design-schema/ -│ └── SKILL.md # Schema and index design workflow -├── hooks/ -│ └── hooks.json # File-change hooks -├── scripts/ -│ └── mongodb-health.sh # Connection and health check script -├── extensions/ # Reserved for extensions -├── mcp.json # MCP server configuration -├── README.md # This file -├── CHANGELOG.md # Version history -└── LICENSE # MIT License +```bash +agent install mongodb ``` -## Usage - -### Rules - -Rules are automatically applied when editing matching files: - -- **`mongodb-schema.mdc`** — Activates when editing `.ts`, `.js` files and files under `models/` or `schemas/` directories. Provides guidance on document modeling, embedding vs referencing, data types, validation, discriminators, schema versioning, sharding, and naming conventions. -- **`mongodb-queries.mdc`** — Activates when editing `.ts` and `.js` files. Covers indexing strategies, projections, aggregation pipelines, cursor-based pagination, bulkWrite, transactions, change streams, explain() analysis, read/write concerns, connection management, and error handling. - -### Agent - -The **MongoDB Schema Design Agent** can be invoked to help with: - -- Designing document schemas from application requirements -- Choosing between embedding and referencing for each relationship -- Building compound, text, geospatial, and partial indexes -- Creating and optimizing aggregation pipelines -- Analyzing query performance with explain() -- Planning sharding strategies and shard key selection -- Configuring Mongoose schemas, middleware, virtuals, and discriminators -- Setting up change streams for real-time data patterns +## Components ### Skills -- **Setup MongoDB** — Follow the step-by-step guide to connect a Node.js/TypeScript app to MongoDB, including Docker setup, connection singletons, model creation, and seeding. -- **Design Schema** — Walk through the complete schema design workflow: identify entities and access patterns, choose embedding vs referencing, apply design patterns (Subset, Bucket, Computed, Extended Reference), create indexes, and validate with explain(). - -### Hooks - -When model or schema files are modified, the plugin notifies you to: - -1. Verify indexes match your query patterns -2. Review migration files before applying them -3. Restart Docker Compose if the MongoDB service configuration changed - -### Health Check Script - -```bash -# Make the script executable -chmod +x plugins/mongodb/scripts/mongodb-health.sh - -# Basic health check (uses MONGODB_URI env var or localhost:27017) -./scripts/mongodb-health.sh - -# Health check with a specific URI -./scripts/mongodb-health.sh "mongodb://localhost:27017/myapp" - -# Detailed status including replica set info -./scripts/mongodb-health.sh --status - -# Show index usage for all collections -./scripts/mongodb-health.sh --indexes -``` +| Skill | Description | +|:------|:------------| +| `setup-mongodb` | Connect Node.js/TypeScript to MongoDB with native driver and Mongoose, including Docker Compose setup | +| `design-schema` | Document schema design, embedding vs referencing, index strategies, and aggregation pipelines | ### MCP Server -The plugin configures the [MongoDB MCP server](https://github.com/mongodb-js/mongodb-mcp-server) for database introspection and management. Set the `MONGODB_URI` environment variable to enable it. - -## Requirements +Provides MongoDB access via `mongodb-mcp-server`. -- Node.js v18 or later -- MongoDB 6.0+ (local, Docker, or MongoDB Atlas) -- mongosh CLI (for health check script) -- One of: - - `mongoose` — for ODM-based development - - `mongodb` — for native driver development +Requires `MONGODB_URI` environment variable. ## License -MIT — see [LICENSE](./LICENSE) for details. +MIT diff --git a/plugins/mongodb/agents/mongodb-schema-agent.md b/plugins/mongodb/agents/mongodb-schema-agent.md deleted file mode 100644 index 4d07e1a..0000000 --- a/plugins/mongodb/agents/mongodb-schema-agent.md +++ /dev/null @@ -1,138 +0,0 @@ -# MongoDB Schema Design Agent - -## Identity - -You are a MongoDB schema design and performance optimization expert. You help developers design efficient document schemas, choose embedding vs referencing strategies, create effective index strategies, build aggregation pipelines, and optimize query performance for both the native MongoDB driver and Mongoose ODM. - -## Expertise - -- MongoDB document schema design and data modeling -- Embedding vs referencing trade-offs -- Index strategies (single-field, compound, multikey, text, geospatial, hashed, partial, wildcard) -- Aggregation pipeline design and optimization -- Query performance analysis with `explain()` -- Sharding strategies and shard key selection -- Change streams and real-time data patterns -- Mongoose ODM (schemas, middleware, virtuals, population, discriminators) -- Transactions and multi-document atomicity -- MongoDB Atlas features (Atlas Search, Data Lake, Charts) - -## Instructions - -### Schema Design - -When helping with schema design: - -1. **Understand access patterns first** — Ask about the application's read and write patterns, query frequency, data growth rate, and consistency requirements before designing any schema. -2. **Design for queries, not normalization** — MongoDB schemas should be optimized for the application's most common queries, not for eliminating data redundancy. -3. **Choose embedding vs referencing intentionally** — Embed data that is always read together and has bounded cardinality. Reference data that is unbounded, frequently updated independently, or queried on its own. -4. **Apply MongoDB design patterns** — Use established patterns where appropriate: - - **Subset Pattern** — embed a subset of related data for fast reads. - - **Bucket Pattern** — group time-series or streaming data into fixed-size buckets. - - **Computed Pattern** — pre-compute derived values to avoid expensive runtime aggregations. - - **Extended Reference Pattern** — duplicate frequently-accessed reference fields to avoid joins. - - **Outlier Pattern** — handle documents that exceed typical bounds with overflow documents. - - **Polymorphic Pattern** — use discriminators for documents with variable shapes in the same collection. - - **Attribute Pattern** — for entities with many rare/variable attributes, store as key-value pairs. - - **Schema Versioning Pattern** — add a `schemaVersion` field to support rolling migrations. -5. **Enforce validation** — Define JSON Schema validation on collections and/or Mongoose schema validators to catch invalid data at the database level. -6. **Use proper data types** — Always use `ObjectId` for references, `Date` for timestamps, `Decimal128` for currency, and enums for constrained string fields. -7. **Plan for scale** — Consider sharding from the start. Choose a shard key with high cardinality and even distribution. Ensure the shard key is included in all queries. -8. **Add timestamps** — Every collection should have `createdAt` and `updatedAt` fields (Mongoose `{ timestamps: true }` handles this automatically). - -### Index Strategy - -When helping with indexes: - -1. **Follow the ESR rule** — Build compound indexes with fields in this order: **E**quality match fields, **S**ort fields, **R**ange filter fields. -2. **Analyze query patterns** — Use `explain('executionStats')` to identify `COLLSCAN` (no index) vs `IXSCAN` (using index). Target `totalDocsExamined` ≈ `nReturned`. -3. **Avoid redundant indexes** — A compound index `{ a: 1, b: 1 }` can satisfy queries on `{ a: 1 }` alone, so a separate `{ a: 1 }` index is redundant. -4. **Use partial indexes** — Index only the documents that match your query patterns to reduce index size: - ```javascript - db.orders.createIndex( - { status: 1, createdAt: -1 }, - { partialFilterExpression: { status: { $in: ['pending', 'processing'] } } } - ); - ``` -5. **Use TTL indexes** for data expiration — Let MongoDB automatically delete expired documents: - ```javascript - db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); - ``` -6. **Consider wildcard indexes** — For schemas with dynamic or unpredictable field names: - ```javascript - db.products.createIndex({ 'attributes.$**': 1 }); - ``` -7. **Monitor index usage** — Use `$indexStats` to identify unused indexes that waste storage and slow writes. -8. **Keep total indexes per collection reasonable** — Each index adds overhead to writes. Aim for fewer than 10-15 indexes per collection. - -### Aggregation Pipeline Design - -When helping with aggregation pipelines: - -1. **Filter early** — Place `$match` as the first stage to leverage indexes and reduce the working set. -2. **Project early** — Use `$project` or `$addFields` early to drop unnecessary fields and reduce memory usage. -3. **Use `$lookup` judiciously** — Cross-collection joins are expensive. Ensure the foreign field is indexed. Use pipeline-form `$lookup` for filtering joined data. -4. **Avoid `$unwind` on large arrays when possible** — It multiplies document count. Use `$filter`, `$map`, or `$reduce` to process arrays in-place. -5. **Use `allowDiskUse: true`** for large datasets — By default, pipeline stages are limited to 100MB of RAM. -6. **Break complex pipelines into stages** — Build and test incrementally for easier debugging. -7. **Consider views or materialized views** — For frequently-run aggregations, create a view or use `$merge`/`$out` to materialize results. - -### Performance Optimization - -When optimizing query performance: - -1. **Examine `explain()` output** — Focus on `executionTimeMillis`, `totalDocsExamined`, `totalKeysExamined`, and `nReturned`. -2. **Detect full collection scans** — Any `COLLSCAN` stage means no index is being used. -3. **Optimize sort operations** — Sorts without index support spill to disk and are slow for large result sets. -4. **Use covered queries** — When projection fields are all in the index, MongoDB can serve the query from the index alone without reading documents. -5. **Monitor slow queries** — Enable the profiler (`db.setProfilingLevel(1, { slowms: 100 })`) and review `system.profile`. -6. **Use `lean()` with Mongoose** — Skip hydration for read-only queries to reduce memory and CPU usage. -7. **Batch operations** — Use `bulkWrite()` instead of individual CRUD calls in loops. -8. **Connection pooling** — Use a single `MongoClient` with appropriate `maxPoolSize`. -9. **Use cursor-based pagination** — Replace `skip()`-based pagination with cursor-based for stable performance. - -### Mongoose-Specific Guidance - -When working with Mongoose: - -1. **Define schemas strictly** — Use `{ strict: true }` (the default) to prevent saving fields not in the schema. -2. **Use middleware wisely** — Pre/post hooks are powerful but can create hidden side effects. Document middleware behavior and keep hooks focused. -3. **Avoid population anti-patterns** — Excessive `populate()` calls create multiple round-trips. For complex joins, use aggregation with `$lookup` instead. -4. **Use virtuals for computed fields** — Don't store values that can be derived: - ```typescript - userSchema.virtual('fullName').get(function() { - return `${this.firstName} ${this.lastName}`; - }); - ``` -5. **Use `toJSON` and `toObject` transforms** — Clean up API responses by removing sensitive fields: - ```typescript - userSchema.set('toJSON', { - transform: (doc, ret) => { - delete ret.password; - delete ret.__v; - return ret; - }, - }); - ``` -6. **Leverage TypeScript** — Use Mongoose's built-in TypeScript support with `HydratedDocument`, `InferSchemaType`, and schema generics for type-safe models. - -## Response Format - -When providing schema designs: -- Present the complete schema definition (Mongoose or native driver). -- Explain the embedding vs referencing decisions and the trade-offs involved. -- List all recommended indexes with rationale. -- Identify potential scaling concerns and how to address them. -- Include sample queries that demonstrate the schema's query efficiency. - -When optimizing performance: -- Show the `explain()` output or summarize key metrics. -- Identify the bottleneck (missing index, COLLSCAN, large `totalDocsExamined`, etc.). -- Provide the fix (new index, query rewrite, schema change, etc.). -- Show the expected improvement. - -When building aggregation pipelines: -- Build the pipeline stage by stage, explaining each stage's purpose. -- Ensure `$match` stages are first and leverage indexes. -- Provide sample input and expected output. -- Note any performance considerations (large `$unwind`, disk usage, etc.). diff --git a/plugins/mongodb/extensions/.gitkeep b/plugins/mongodb/extensions/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/mongodb/hooks/hooks.json b/plugins/mongodb/hooks/hooks.json deleted file mode 100644 index 490a70a..0000000 --- a/plugins/mongodb/hooks/hooks.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "event": "file-changed", - "match": ["**/models/**/*.ts", "**/models/**/*.js", "**/schemas/**/*.ts", "**/schemas/**/*.js"], - "actions": [ - { - "type": "notify", - "message": "MongoDB model changed. Ensure indexes are defined on the schema and match your query patterns. Run 'db.collection.getIndexes()' to verify.", - "severity": "info" - } - ] - }, - { - "event": "file-changed", - "match": ["**/migrations/**/*.ts", "**/migrations/**/*.js"], - "actions": [ - { - "type": "notify", - "message": "MongoDB migration file changed. Review the migration carefully before running it against your database.", - "severity": "warning" - } - ] - }, - { - "event": "file-changed", - "match": ["docker-compose.yml", "docker-compose.yaml"], - "actions": [ - { - "type": "notify", - "message": "Docker Compose file changed. If MongoDB service was modified, restart with 'docker compose up -d mongodb'.", - "severity": "info" - } - ] - } - ] -} diff --git a/plugins/mongodb/rules/mongodb-queries.mdc b/plugins/mongodb/rules/mongodb-queries.mdc deleted file mode 100644 index ca434e4..0000000 --- a/plugins/mongodb/rules/mongodb-queries.mdc +++ /dev/null @@ -1,443 +0,0 @@ ---- -description: Best practices for MongoDB queries, aggregation pipelines, indexing, transactions, and performance optimization -globs: - - "**/*.ts" - - "**/*.js" -alwaysApply: false ---- - -# MongoDB Query Best Practices - -## Create Indexes for Query Patterns - -- **Every query should be backed by an index** — queries without indexes perform full collection scans. -- Create indexes that match your `find()` filter, sort, and projection fields (the **ESR rule**: Equality → Sort → Range). -- Use **compound indexes** for queries that filter and sort on multiple fields. -- Use `explain('executionStats')` to verify index usage. - -```typescript -// Create indexes that match your query patterns -// If you query: db.orders.find({ status: 'pending', customerId: id }).sort({ createdAt: -1 }) -// Create this compound index: -db.collection('orders').createIndex( - { status: 1, customerId: 1, createdAt: -1 }, - { name: 'idx_orders_status_customer_date' } -); - -// Mongoose: define indexes on the schema -orderSchema.index({ status: 1, customerId: 1, createdAt: -1 }); - -// Verify index usage -const explanation = await Order.find({ status: 'pending' }) - .sort({ createdAt: -1 }) - .explain('executionStats'); -console.log(explanation.executionStats.executionStages.stage); // should be IXSCAN, not COLLSCAN -``` - -## Use Projection to Limit Fields - -- **Always project only the fields you need** — transferring unnecessary data wastes bandwidth and memory. -- A projection can also leverage a **covered query** (index-only query) if all requested fields are in the index. - -```typescript -// BAD: fetching all fields when you only need name and email -const users = await User.find({ active: true }); - -// GOOD: project only needed fields -const users = await User.find({ active: true }) - .select('name email') - .lean(); - -// Native driver -const users = await db.collection('users') - .find({ active: true }, { projection: { name: 1, email: 1, _id: 0 } }) - .toArray(); -``` - -## Use Aggregation Pipeline for Complex Transformations - -- Prefer the **aggregation pipeline** over application-side data processing for grouping, reshaping, and computing. -- Order stages for performance: `$match` and `$project` early to reduce the working set. -- Use `$lookup` for cross-collection joins — but keep them minimal and indexed. - -```typescript -// Aggregation: monthly revenue report -const report = await Order.aggregate([ - // 1. Filter early to reduce working set - { $match: { - status: 'delivered', - createdAt: { $gte: startOfYear, $lte: endOfYear }, - }}, - // 2. Group by month - { $group: { - _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } }, - totalRevenue: { $sum: { $toDecimal: '$total' } }, - orderCount: { $sum: 1 }, - avgOrderValue: { $avg: { $toDecimal: '$total' } }, - }}, - // 3. Sort by month - { $sort: { _id: 1 } }, - // 4. Reshape output - { $project: { - month: '$_id', - totalRevenue: { $round: ['$totalRevenue', 2] }, - orderCount: 1, - avgOrderValue: { $round: ['$avgOrderValue', 2] }, - _id: 0, - }}, -]); -``` - -```typescript -// $lookup: join orders with customer details -const enrichedOrders = await Order.aggregate([ - { $match: { status: 'pending' } }, - { $lookup: { - from: 'customers', - localField: 'customerId', - foreignField: '_id', - as: 'customer', - pipeline: [ - { $project: { name: 1, email: 1, tier: 1 } }, - ], - }}, - { $unwind: '$customer' }, -]); -``` - -## Handle Cursor-Based Pagination - -- **Avoid `skip()` for large datasets** — `skip(N)` forces MongoDB to scan and discard N documents on every query. -- Use **cursor-based pagination** (also called keyset pagination) for consistent performance at any page depth. -- Paginate using a unique, indexed field (e.g., `_id`, `createdAt` combined with `_id`). - -```typescript -// BAD: skip-based pagination — degrades at high offsets -const page = await Post.find({ published: true }) - .sort({ createdAt: -1 }) - .skip((pageNumber - 1) * pageSize) - .limit(pageSize); - -// GOOD: cursor-based pagination — consistent performance -async function getNextPage(lastId?: string, pageSize = 20) { - const query: any = { published: true }; - if (lastId) { - query._id = { $lt: new Types.ObjectId(lastId) }; - } - const results = await Post.find(query) - .sort({ _id: -1 }) - .limit(pageSize + 1) // fetch one extra to detect if there's a next page - .lean(); - - const hasNextPage = results.length > pageSize; - const items = hasNextPage ? results.slice(0, -1) : results; - const nextCursor = hasNextPage ? items[items.length - 1]._id.toString() : null; - - return { items, nextCursor, hasNextPage }; -} -``` - -## Use bulkWrite for Batch Operations - -- Use **`bulkWrite()`** for batch inserts, updates, and deletes — it sends all operations in a single round-trip. -- Prefer `ordered: false` when operations are independent — it allows MongoDB to execute them in parallel. - -```typescript -// BAD: individual operations in a loop -for (const item of items) { - await Product.updateOne( - { sku: item.sku }, - { $set: { price: item.price, updatedAt: new Date() } }, - { upsert: true } - ); -} - -// GOOD: batch all operations in a single bulkWrite -const operations = items.map(item => ({ - updateOne: { - filter: { sku: item.sku }, - update: { $set: { price: item.price, updatedAt: new Date() } }, - upsert: true, - }, -})); - -const result = await Product.bulkWrite(operations, { ordered: false }); -console.log(`Matched: ${result.matchedCount}, Upserted: ${result.upsertedCount}`); -``` - -## Handle Duplicate Key Errors - -- Always handle **`E11000` duplicate key errors** gracefully — they occur on unique index violations. -- Use `upsert` when appropriate to avoid separate find-then-insert patterns. -- Catch and handle the error with a clear message. - -```typescript -import { MongoServerError } from 'mongodb'; - -async function createUser(data: CreateUserInput) { - try { - const user = await User.create(data); - return user; - } catch (error) { - if (error instanceof MongoServerError && error.code === 11000) { - const field = Object.keys(error.keyPattern)[0]; - throw new ConflictError(`A user with that ${field} already exists`); - } - throw error; - } -} - -// Alternative: use findOneAndUpdate with upsert -async function upsertUser(email: string, data: Partial<UserInput>) { - return User.findOneAndUpdate( - { email }, - { $set: data, $setOnInsert: { createdAt: new Date() } }, - { upsert: true, new: true, runValidators: true } - ); -} -``` - -## Use Transactions for Multi-Document Atomicity - -- Use **transactions** when you need atomicity across multiple documents or collections. -- Transactions require a **replica set** or sharded cluster (not standalone). -- Keep transactions **short** — long-running transactions hold locks and degrade performance. -- Always handle `TransientTransactionError` and `UnknownTransactionCommitResult` with retry logic. - -```typescript -// Mongoose transaction: transfer funds between accounts -async function transferFunds(fromId: string, toId: string, amount: number) { - const session = await mongoose.startSession(); - try { - session.startTransaction({ - readConcern: { level: 'snapshot' }, - writeConcern: { w: 'majority' }, - }); - - const from = await Account.findOneAndUpdate( - { _id: fromId, balance: { $gte: amount } }, - { $inc: { balance: -amount } }, - { session, new: true } - ); - - if (!from) { - throw new Error('Insufficient funds or account not found'); - } - - await Account.findOneAndUpdate( - { _id: toId }, - { $inc: { balance: amount } }, - { session } - ); - - await TransactionLog.create([{ - from: fromId, - to: toId, - amount, - type: 'transfer', - timestamp: new Date(), - }], { session }); - - await session.commitTransaction(); - } catch (error) { - await session.abortTransaction(); - throw error; - } finally { - session.endSession(); - } -} -``` - -## Use Change Streams for Reactive Patterns - -- Use **change streams** to react to real-time data changes without polling. -- Change streams require a replica set or sharded cluster. -- Always store and use **resume tokens** for fault-tolerant stream consumption. - -```typescript -// Watch for new orders and trigger processing -const pipeline = [ - { $match: { - operationType: { $in: ['insert', 'update'] }, - 'fullDocument.status': 'pending', - }}, -]; - -const changeStream = Order.watch(pipeline, { fullDocument: 'updateLookup' }); - -changeStream.on('change', async (change) => { - console.log(`New/updated order: ${change.fullDocument._id}`); - await processOrder(change.fullDocument); -}); - -changeStream.on('error', (error) => { - console.error('Change stream error:', error); - // Reconnect using the resume token - const resumeToken = changeStream.resumeToken; - // restart stream with { resumeAfter: resumeToken } -}); -``` - -## Optimize with explain() - -- Use **`explain('executionStats')`** to understand query performance. -- Look for: `COLLSCAN` (bad — no index), `IXSCAN` (good — using index), `totalDocsExamined` vs `nReturned`. -- The goal: `totalDocsExamined` should be close to `nReturned`. - -```typescript -// Check if your query uses an index -const plan = await User.find({ email: 'alice@example.com' }) - .explain('executionStats'); - -const stats = plan.executionStats; -console.log({ - executionTimeMs: stats.executionTimeMillis, - totalDocsExamined: stats.totalDocsExamined, - totalKeysExamined: stats.totalKeysExamined, - nReturned: stats.nReturned, - indexUsed: stats.executionStages.inputStage?.indexName ?? 'NONE (COLLSCAN)', -}); -``` - -## Set Read/Write Concerns Appropriately - -- **Write concern** — controls durability guarantee: - - `w: 1` (default) — acknowledged by the primary. - - `w: 'majority'` — acknowledged by a majority of replica set members (safer). - - `w: 0` — fire-and-forget (fastest, but no error detection). - - `j: true` — wait for journal commit (protects against power loss). -- **Read concern** — controls consistency of reads: - - `local` (default) — returns the most recent data available on the node. - - `majority` — returns data that has been acknowledged by a majority (avoids reading rollback-prone data). - - `snapshot` — for transactions; provides a consistent point-in-time view. -- **Read preference** — controls which node to read from: - - `primary` (default) — always read from primary. - - `primaryPreferred` — read from primary, fallback to secondary. - - `secondary` — read from secondaries (for read scaling or analytics). - - `nearest` — read from the lowest-latency node. - -```typescript -// Configure per-operation -const user = await User.findOne({ email }) - .read('secondaryPreferred') - .lean(); - -// Configure at connection level -mongoose.connect(uri, { - readPreference: 'secondaryPreferred', - w: 'majority', - journal: true, - readConcern: { level: 'majority' }, -}); -``` - -## Native Driver vs Mongoose - -### Use the Native Driver When - -- You need **maximum performance** and minimal overhead. -- You are working with **raw aggregation pipelines** or database-level operations. -- You want fine-grained control over every operation. -- You are building **infrastructure or tooling** rather than application code. - -```typescript -import { MongoClient } from 'mongodb'; - -const client = new MongoClient(uri); -const db = client.db('myapp'); -const users = db.collection('users'); - -// Direct, no overhead -const result = await users.findOne({ email: 'alice@example.com' }); -``` - -### Use Mongoose When - -- You want **schema validation** and type safety at the application level. -- You need **middleware** (pre/post hooks), **virtuals**, **getters/setters**, and **population**. -- You value **developer experience** — Mongoose's API is more expressive for CRUD-heavy apps. -- You are building a **typical web application** with well-defined models. - -```typescript -import mongoose, { Schema, model } from 'mongoose'; - -const userSchema = new Schema({ - email: { type: String, required: true, unique: true }, - name: String, - role: { type: String, enum: ['user', 'admin'], default: 'user' }, -}, { timestamps: true }); - -const User = model('User', userSchema); -const user = await User.findOne({ email: 'alice@example.com' }); -``` - -## Connection Management - -- Create **one `MongoClient` or `mongoose.connect()` call** for the entire application — never create connections per request. -- Use **connection pooling** (default pool size is 100 for the native driver, 5 for Mongoose). -- Handle connection errors and implement reconnection logic. -- Close the connection gracefully on application shutdown. - -```typescript -// Mongoose: singleton connection -import mongoose from 'mongoose'; - -let isConnected = false; - -export async function connectToDatabase() { - if (isConnected) return; - - await mongoose.connect(process.env.MONGODB_URI!, { - maxPoolSize: 10, - minPoolSize: 2, - serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 45000, - retryWrites: true, - w: 'majority', - }); - - isConnected = true; - console.log('Connected to MongoDB'); -} - -// Graceful shutdown -process.on('SIGTERM', async () => { - await mongoose.connection.close(); - console.log('MongoDB connection closed'); - process.exit(0); -}); -``` - -## Error Handling - -- Always handle MongoDB-specific errors with proper error codes. -- Implement **retry logic** for transient errors (network timeouts, elections). -- Never expose raw MongoDB errors to end users. - -```typescript -import { MongoServerError, MongoNetworkError } from 'mongodb'; - -async function safeDbOperation<T>(operation: () => Promise<T>, retries = 3): Promise<T> { - for (let attempt = 1; attempt <= retries; attempt++) { - try { - return await operation(); - } catch (error) { - if (error instanceof MongoNetworkError && attempt < retries) { - const delay = Math.pow(2, attempt) * 100; - console.warn(`Transient error, retrying in ${delay}ms (attempt ${attempt}/${retries})`); - await new Promise(resolve => setTimeout(resolve, delay)); - continue; - } - if (error instanceof MongoServerError) { - switch (error.code) { - case 11000: throw new ConflictError('Duplicate key violation'); - case 121: throw new ValidationError('Document failed validation'); - default: throw new DatabaseError(`Database error: ${error.message}`); - } - } - throw error; - } - } - throw new Error('Exhausted retries'); -} -``` diff --git a/plugins/mongodb/rules/mongodb-schema.mdc b/plugins/mongodb/rules/mongodb-schema.mdc deleted file mode 100644 index 4ed417e..0000000 --- a/plugins/mongodb/rules/mongodb-schema.mdc +++ /dev/null @@ -1,297 +0,0 @@ ---- -description: Best practices for MongoDB schema design, document modeling, embedding vs referencing, validation, and data types -globs: - - "**/*.ts" - - "**/*.js" - - "**/models/**" - - "**/schemas/**" -alwaysApply: false ---- - -# MongoDB Schema Design Best Practices - -## Design for Your Queries, Not Normalization - -- MongoDB is **not** a relational database — do not blindly normalize data into separate collections. -- Identify your application's **read and write patterns** first, then design the schema to support them efficiently. -- Optimize for the most frequent queries — denormalization is expected and encouraged when it reduces query complexity. -- Ask: "What questions will my application ask of this data?" and model accordingly. - -## Embedding vs Referencing - -### Embed When - -- The related data is **always read together** with the parent document (e.g., shipping address on an order). -- The related data has a **bounded, small cardinality** (e.g., a user's phone numbers, an order's line items). -- The related data **does not change independently** of the parent document. -- You want **atomic reads** — a single query returns all needed data. - -```typescript -// GOOD: Embed address inside user — always read together -const userSchema = new Schema({ - name: String, - email: { type: String, unique: true }, - addresses: [{ - label: { type: String, enum: ['home', 'work', 'other'] }, - street: String, - city: String, - state: String, - zip: String, - country: { type: String, default: 'US' }, - }], -}); -``` - -### Reference When - -- The related data is **large or unbounded** (e.g., blog comments, log entries). -- The related data is **frequently updated independently** of the parent. -- The related data **needs to be queried on its own** (e.g., finding all comments by a user). -- The related data is **shared across multiple parent documents** (e.g., categories, tags). - -```typescript -// GOOD: Reference comments — unbounded, queried independently -const postSchema = new Schema({ - title: String, - content: String, - author: { type: Schema.Types.ObjectId, ref: 'User' }, -}); - -const commentSchema = new Schema({ - post: { type: Schema.Types.ObjectId, ref: 'Post', index: true }, - author: { type: Schema.Types.ObjectId, ref: 'User', index: true }, - body: String, - createdAt: { type: Date, default: Date.now }, -}); -``` - -### Hybrid Pattern (Subset Pattern) - -- Embed a **subset** of the referenced data for fast reads; store the full data in a separate collection. -- Use this for data that is read frequently but changes rarely (e.g., embed the latest 5 reviews on a product, store all reviews in a reviews collection). - -```typescript -// Hybrid: embed last 3 reviews for fast display, full reviews in separate collection -const productSchema = new Schema({ - name: String, - price: Schema.Types.Decimal128, - recentReviews: [{ - _id: Schema.Types.ObjectId, - rating: Number, - summary: String, - author: String, - }], - reviewCount: { type: Number, default: 0 }, -}); -``` - -## Document Size - -- MongoDB has a hard limit of **16 MB per document** — design schemas to stay well under this limit. -- Avoid patterns that allow arrays or embedded documents to grow without bound. -- Use the **Bucket Pattern** for time-series or high-volume data: - -```typescript -// Bucket Pattern: group sensor readings by hour -const sensorBucketSchema = new Schema({ - sensorId: { type: String, index: true }, - startDate: Date, - endDate: Date, - readings: [{ - timestamp: Date, - value: Number, - }], - count: Number, // track count for efficient bucket rotation -}); -``` - -## Use Proper Data Types - -- Use **`ObjectId`** for references between documents — never store IDs as plain strings. -- Use **`Date`** for timestamps — never store dates as strings or Unix timestamps. -- Use **`Decimal128`** for currency and financial values — `Number` (IEEE 754 double) cannot represent all decimal values exactly. -- Use **`Boolean`** for true/false flags — never use `0`/`1` or `"true"`/`"false"` strings. -- Use **`Int32`** or **`Long`** for integer counters — avoid floating-point `Number` for values that should never be fractional. -- Use **`Buffer`** / **`BinData`** for binary data — never Base64-encode into strings. - -```typescript -const orderSchema = new Schema({ - orderNumber: { type: String, unique: true }, - customer: { type: Schema.Types.ObjectId, ref: 'Customer', required: true }, - total: { type: Schema.Types.Decimal128, required: true }, - items: [{ - product: { type: Schema.Types.ObjectId, ref: 'Product' }, - quantity: { type: Number, min: 1 }, - unitPrice: Schema.Types.Decimal128, - }], - status: { - type: String, - enum: ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled'], - default: 'pending', - }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, -}); -``` - -## Add Validation with JSON Schema - -- Use MongoDB's **JSON Schema validation** or Mongoose validators to enforce data integrity at the database level. -- Define `required` fields, `enum` constraints, `min`/`max` values, and `pattern` regex. -- Validate at the schema level — do not rely solely on application code. - -```typescript -// Mongoose validation -const userSchema = new Schema({ - email: { - type: String, - required: [true, 'Email is required'], - unique: true, - lowercase: true, - trim: true, - match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email'], - }, - age: { - type: Number, - min: [0, 'Age cannot be negative'], - max: [150, 'Age seems unrealistic'], - }, - role: { - type: String, - enum: { - values: ['user', 'admin', 'moderator'], - message: '{VALUE} is not a valid role', - }, - default: 'user', - }, -}); -``` - -```javascript -// Native driver: JSON Schema collection validation -db.createCollection('users', { - validator: { - $jsonSchema: { - bsonType: 'object', - required: ['email', 'name', 'role'], - properties: { - email: { - bsonType: 'string', - pattern: '^\\S+@\\S+\\.\\S+$', - description: 'Must be a valid email address', - }, - role: { - bsonType: 'string', - enum: ['user', 'admin', 'moderator'], - }, - age: { - bsonType: 'int', - minimum: 0, - maximum: 150, - }, - }, - }, - }, - validationAction: 'error', - validationLevel: 'strict', -}); -``` - -## Use Discriminators for Polymorphism - -- Use **discriminators** to store different document shapes in the same collection while sharing a common base schema. -- Prefer discriminators over separate collections when entities share most fields but differ in a few. - -```typescript -const eventSchema = new Schema({ - timestamp: { type: Date, default: Date.now }, - user: { type: Schema.Types.ObjectId, ref: 'User' }, -}, { discriminatorKey: 'kind' }); - -const Event = model('Event', eventSchema); - -const ClickEvent = Event.discriminator('ClickEvent', new Schema({ - element: String, - page: String, - coordinates: { x: Number, y: Number }, -})); - -const PurchaseEvent = Event.discriminator('PurchaseEvent', new Schema({ - product: { type: Schema.Types.ObjectId, ref: 'Product' }, - amount: Schema.Types.Decimal128, - currency: { type: String, default: 'USD' }, -})); -``` - -## Version Your Schemas - -- Add a **`schemaVersion`** field to documents to enable graceful migration of document shapes over time. -- Handle multiple schema versions in application code during rolling migrations. -- Never assume all documents in a collection have the same shape. - -```typescript -const userSchema = new Schema({ - schemaVersion: { type: Number, default: 2 }, - name: String, // v1: was 'firstName' + 'lastName' - email: String, - // ... other fields -}); - -// Migration middleware: upgrade v1 documents on read -userSchema.post('init', function (doc) { - if (doc.schemaVersion === 1) { - doc.name = `${doc.firstName} ${doc.lastName}`; - doc.schemaVersion = 2; - doc.save(); - } -}); -``` - -## Plan for Sharding from the Start - -- Choose your **shard key** early — it is **extremely difficult** to change after data is distributed. -- Select a shard key with **high cardinality**, **even distribution**, and **query isolation** (most queries include the shard key). -- Avoid monotonically increasing shard keys (`_id`, timestamps) as the sole shard key — they cause hot spots. -- Consider **hashed shard keys** for even distribution or **compound shard keys** for query targeting. - -```javascript -// Hashed shard key for even distribution -sh.shardCollection('mydb.events', { userId: 'hashed' }); - -// Compound shard key for query targeting + distribution -sh.shardCollection('mydb.orders', { customerId: 1, createdAt: 1 }); -``` - -## Field Naming Conventions - -- Use **camelCase** for field names (e.g., `firstName`, `orderTotal`, `createdAt`). -- **Never** use `$` as a field name prefix — it conflicts with MongoDB operators. -- **Never** use `.` in field names — it conflicts with dot-notation path syntax. -- Keep field names **short but descriptive** — they are stored in every document and affect storage size. -- Use **consistent naming** across collections (e.g., always `createdAt`, not sometimes `created_at`). -- Always include `createdAt` and `updatedAt` timestamps on every document. - -```typescript -// Mongoose: auto-manage timestamps -const schema = new Schema( - { name: String, email: String }, - { timestamps: true } // adds createdAt and updatedAt automatically -); -``` - -## General Schema Guidelines - -- **Keep documents reasonably flat** — avoid deep nesting beyond 3-4 levels. -- **Use lean queries** when you don't need Mongoose document features: - ```typescript - const users = await User.find({ active: true }).lean(); - ``` -- **Separate hot and cold data** — archive or move infrequently accessed data to keep working sets small. -- **Use TTL indexes** for data that should expire automatically: - ```typescript - sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); - ``` -- **Use capped collections** for fixed-size logging or streaming scenarios: - ```javascript - db.createCollection('logs', { capped: true, size: 1048576, max: 5000 }); - ``` diff --git a/plugins/mongodb/scripts/mongodb-health.sh b/plugins/mongodb/scripts/mongodb-health.sh deleted file mode 100755 index 3992298..0000000 --- a/plugins/mongodb/scripts/mongodb-health.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash -# -# mongodb-health.sh — Check MongoDB connection health, replica set status, and basic diagnostics -# -# Usage: -# ./scripts/mongodb-health.sh # Check health using MONGODB_URI env var -# ./scripts/mongodb-health.sh <connection-uri> # Check health using provided URI -# ./scripts/mongodb-health.sh --status # Show detailed replica set and server status -# ./scripts/mongodb-health.sh --indexes <db> # Show index usage for all collections in a database -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*"; } -log_title() { echo -e "\n${BLUE}=== $* ===${NC}"; } - -# Check prerequisites -if ! command -v mongosh &> /dev/null; then - log_error "mongosh is not installed." - echo " Install via: brew install mongosh (macOS) or see https://www.mongodb.com/docs/mongodb-shell/install/" - echo " Or use Docker: docker exec -it mongodb-dev mongosh" - exit 1 -fi - -# Determine connection URI -MONGO_URI="${1:-${MONGODB_URI:-mongodb://localhost:27017}}" -COMMAND="${1:-}" - -# Remove command flags from URI -if [[ "$COMMAND" == --* ]]; then - MONGO_URI="${MONGODB_URI:-mongodb://localhost:27017}" -fi - -# --- Ping Test --- -check_connection() { - log_title "Connection Check" - if mongosh "$MONGO_URI" --quiet --eval "db.runCommand({ ping: 1 }).ok" 2>/dev/null | grep -q "1"; then - log_info "MongoDB is reachable at: $MONGO_URI" - else - log_error "Cannot connect to MongoDB at: $MONGO_URI" - exit 1 - fi -} - -# --- Basic Server Info --- -show_server_info() { - log_title "Server Information" - mongosh "$MONGO_URI" --quiet --eval " - const info = db.serverStatus(); - print(' Version: ' + info.version); - print(' Uptime: ' + Math.round(info.uptime / 3600) + ' hours'); - print(' Connections: ' + info.connections.current + ' current / ' + info.connections.available + ' available'); - print(' Storage Engine:' + info.storageEngine.name); - " 2>/dev/null || log_warn "Could not retrieve server info (may lack permissions)" -} - -# --- Database Stats --- -show_db_stats() { - log_title "Database Statistics" - mongosh "$MONGO_URI" --quiet --eval " - const stats = db.stats(); - print(' Database: ' + stats.db); - print(' Collections: ' + stats.collections); - print(' Documents: ' + stats.objects); - print(' Data Size: ' + (stats.dataSize / 1024 / 1024).toFixed(2) + ' MB'); - print(' Storage Size: ' + (stats.storageSize / 1024 / 1024).toFixed(2) + ' MB'); - print(' Index Size: ' + (stats.indexSize / 1024 / 1024).toFixed(2) + ' MB'); - print(' Indexes: ' + stats.indexes); - " 2>/dev/null || log_warn "Could not retrieve database stats" -} - -# --- Collection Details --- -show_collections() { - log_title "Collections" - mongosh "$MONGO_URI" --quiet --eval " - const colls = db.getCollectionInfos(); - if (colls.length === 0) { - print(' No collections found.'); - } else { - colls.forEach(c => { - const stats = db.getCollection(c.name).stats(); - print(' ' + c.name.padEnd(30) + ' docs: ' + String(stats.count).padStart(8) + ' size: ' + (stats.size / 1024).toFixed(1).padStart(8) + ' KB indexes: ' + stats.nindexes); - }); - } - " 2>/dev/null || log_warn "Could not retrieve collection info" -} - -# --- Replica Set Status --- -show_replica_status() { - log_title "Replica Set Status" - mongosh "$MONGO_URI" --quiet --eval " - try { - const status = rs.status(); - print(' Set Name: ' + status.set); - print(' Members:'); - status.members.forEach(m => { - const state = m.stateStr.padEnd(12); - const health = m.health === 1 ? 'healthy' : 'UNHEALTHY'; - print(' ' + m.name.padEnd(30) + ' ' + state + ' ' + health); - }); - } catch (e) { - print(' Not a replica set (standalone instance)'); - } - " 2>/dev/null || log_warn "Could not retrieve replica set status" -} - -# --- Index Usage --- -show_index_usage() { - local db_name="${2:-}" - log_title "Index Usage Statistics" - if [ -z "$db_name" ]; then - db_name=$(mongosh "$MONGO_URI" --quiet --eval "db.getName()" 2>/dev/null) - fi - mongosh "$MONGO_URI" --quiet --eval " - const colls = db.getCollectionNames(); - colls.forEach(collName => { - print('\\n Collection: ' + collName); - const indexes = db.getCollection(collName).aggregate([{\$indexStats: {}}]).toArray(); - if (indexes.length === 0) { - print(' No index stats available'); - } else { - indexes.forEach(idx => { - const ops = String(idx.accesses.ops).padStart(8); - print(' ' + idx.name.padEnd(40) + ' ops: ' + ops + ' since: ' + idx.accesses.since.toISOString().split('T')[0]); - }); - } - }); - " 2>/dev/null || log_warn "Could not retrieve index stats (requires replica set)" -} - -# --- Main --- -case "${COMMAND}" in - --status) - check_connection - show_server_info - show_db_stats - show_collections - show_replica_status - ;; - --indexes) - check_connection - show_index_usage "$@" - ;; - *) - check_connection - show_server_info - show_db_stats - show_collections - ;; -esac - -echo "" -log_info "Health check complete." diff --git a/plugins/sentry/.cursor/plugin.json b/plugins/sentry/.cursor/plugin.json index 9ab2df0..26c2fbd 100644 --- a/plugins/sentry/.cursor/plugin.json +++ b/plugins/sentry/.cursor/plugin.json @@ -1,18 +1,27 @@ { "name": "sentry", "version": "1.0.0", - "description": "Cursor plugin for Sentry — error monitoring, performance tracking, session replay, and alerting", - "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "description": "Cursor plugin for Sentry \u2014 error monitoring, performance tracking, session replay, and alerting", + "author": { + "name": "Cursor", + "email": "plugins@cursor.com" + }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["sentry", "error-monitoring", "performance", "observability", "debugging"], + "keywords": [ + "sentry", + "error-monitoring", + "performance", + "observability", + "debugging" + ], "category": "observability", - "tags": ["monitoring", "error-tracking", "performance"], - "agents": "./agents/", + "tags": [ + "monitoring", + "error-tracking", + "performance" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index f6d2c01..5f2859d 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,30 +1,13 @@ # Changelog -All notable changes to the Sentry Cursor plugin will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + ## [1.0.0] - 2026-02-07 ### Added - -- **Plugin manifest** (`.cursor/plugin.json`) with full metadata, keywords, and category. -- **Rules** - - `sentry-integration.mdc` — Best practices for Sentry SDK initialization, DSN management, breadcrumbs, user context, error capturing with tags/extra/contexts, scoped context with `Sentry.withScope`, source map configuration, `beforeSend` data filtering, React error boundaries, and environment/release tagging. - - `sentry-performance.mdc` — Best practices for custom transactions, child spans, meaningful transaction naming, `tracesSampleRate` configuration, CPU profiling with `@sentry/profiling-node`, and Web Vitals tracking. -- **Agents** - - `sentry-debug-agent.md` — Specialized agent for triaging Sentry issues, analyzing stack traces and breadcrumbs, identifying common error patterns, and suggesting concrete fixes. -- **Skills** - - `setup-sentry/SKILL.md` — Framework-specific Sentry setup for Next.js, React (Vite), Node.js (Express), and Python (Django/Flask/FastAPI), including source map configuration, error boundaries, and release management. - - `configure-alerts/SKILL.md` — Guide for configuring issue alerts, metric alerts, uptime monitors, and cron monitors via UI and API, with recommended alert rules and routing best practices. -- **Hooks** - - `hooks.json` — Pre-deploy hook definition that runs `sentry-release.sh` to create releases, upload source maps, and record deployments. -- **Scripts** - - `sentry-release.sh` — Shell script for creating Sentry releases with commit association, source map upload, finalization, and deployment recording. -- **MCP Server** - - `mcp.json` — Configuration for the `@sentry/mcp-server` with tools for listing issues, fetching events, managing releases, and querying performance data. -- **Documentation** - - `README.md` — Plugin overview, directory structure, setup instructions, and usage guide. - - `CHANGELOG.md` — This file. - - `LICENSE` — MIT license. +- Initial plugin with skills and MCP server configuration diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md index cde0572..c0b100d 100644 --- a/plugins/sentry/README.md +++ b/plugins/sentry/README.md @@ -1,117 +1,28 @@ -# Sentry Plugin for Cursor +# Sentry Plugin -A Cursor plugin that helps developers integrate [Sentry](https://sentry.io) error monitoring, performance tracking, session replay, and alerting into their projects. +Cursor plugin for Sentry — error monitoring, performance tracking, session replay, and alerting. -## Features +## Installation -- **Rules** — Coding best practices for Sentry SDK integration and performance monitoring, applied automatically when editing JS/TS files. -- **Agents** — A specialized debug agent that triages Sentry issues, analyzes stack traces and breadcrumbs, and suggests fixes. -- **Skills** — Step-by-step guides for setting up Sentry in Next.js, React, Node.js, and Python projects, and for configuring alerts. -- **Hooks** — Pre-deploy hook that creates Sentry releases, uploads source maps, and records deployments. -- **MCP Server** — Connects to the Sentry MCP server to expose issues, events, releases, and performance data directly to AI agents. - -## Directory Structure - -``` -plugins/sentry/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── sentry-debug-agent.md # Debug/triage agent -├── extensions/ # Reserved for future extensions -├── hooks/ -│ └── hooks.json # Pre-deploy hook definitions -├── mcp.json # MCP server configuration -├── rules/ -│ ├── sentry-integration.mdc # SDK integration best practices -│ └── sentry-performance.mdc # Performance monitoring best practices -├── scripts/ -│ └── sentry-release.sh # Release creation script -├── skills/ -│ ├── configure-alerts/ -│ │ └── SKILL.md # Alert configuration guide -│ └── setup-sentry/ -│ └── SKILL.md # Project setup guide -├── CHANGELOG.md -├── LICENSE -└── README.md +```bash +agent install sentry ``` -## Quick Start - -### 1. Install the Plugin - -Copy or symlink the `plugins/sentry/` directory into your Cursor workspace. - -### 2. Set Environment Variables - -The plugin requires the following environment variables for full functionality: - -| Variable | Required | Description | -|---|---|---| -| `SENTRY_DSN` | Yes | Sentry project DSN | -| `SENTRY_ORG` | For releases/MCP | Sentry organization slug | -| `SENTRY_PROJECT` | For releases/MCP | Sentry project slug | -| `SENTRY_AUTH_TOKEN` | For releases/MCP | Sentry API auth token | -| `SENTRY_ENVIRONMENT` | No | Deployment environment (default: `production`) | -| `SENTRY_RELEASE` | No | Release version (default: git short SHA) | - -### 3. Use the Plugin - -- **Rules** activate automatically when you edit `.ts`, `.tsx`, `.js`, or `.jsx` files, providing inline guidance on Sentry best practices. -- **Skills** are available as step-by-step guides — ask Cursor to "set up Sentry" or "configure Sentry alerts." -- **The debug agent** can be invoked to triage Sentry issues — paste an error from Sentry and ask for analysis. -- **Hooks** run automatically during deployment workflows to create releases and upload source maps. -- **MCP Server** connects Cursor's AI to your Sentry data for querying issues, events, and performance metrics. - -## Rules - -### `sentry-integration.mdc` - -Best practices for SDK initialization, DSN management, breadcrumbs, user context, error capturing, `beforeSend` filtering, React error boundaries, source maps, and release/environment tagging. - -### `sentry-performance.mdc` - -Best practices for custom transactions, child spans, transaction naming, sample rate configuration, CPU profiling, and Web Vitals tracking. - -## Skills - -### Setup Sentry (`skills/setup-sentry/SKILL.md`) - -Framework-specific setup instructions for: -- **Next.js** — Wizard-based setup, client/server config, source maps, error boundaries. -- **React (Vite)** — Manual SDK setup, Vite plugin for source maps, error boundary. -- **Node.js (Express)** — Instrumentation, Express error handler, profiling. -- **Python (Django / Flask / FastAPI)** — `sentry-sdk` init, auto-instrumentation. - -### Configure Alerts (`skills/configure-alerts/SKILL.md`) - -- Issue alerts (new errors, regressions, volume spikes) -- Metric alerts (error rates, latency, Apdex, failure rates, crash-free rate) -- Uptime monitors (HTTP health checks) -- Cron monitors (scheduled job tracking) -- Alert routing best practices - -## Pre-Deploy Hook +## Components -The `sentry-release.sh` script (triggered by `hooks.json`) performs: +### Skills -1. Creates a new Sentry release -2. Associates git commits -3. Uploads source maps -4. Finalizes the release -5. Records the deployment +| Skill | Description | +|:------|:------------| +| `setup-sentry` | Framework-specific setup for Next.js, React, Node.js, and Python with source maps and releases | +| `configure-alerts` | Issue alerts, metric alerts, uptime monitors, and cron monitors | -## MCP Server +### MCP Server -The plugin uses the official `@sentry/mcp-server` package. Available tools: +Provides access to Sentry via `@sentry/mcp-server`. -- `list_issues` — Query unresolved issues -- `get_issue` / `get_event` — Detailed issue and event data -- `search_events` — Search with Sentry query syntax -- `resolve_issue` / `assign_issue` — Issue management -- `get_performance_summary` — Transaction performance metrics +Requires `SENTRY_AUTH_TOKEN` environment variable. ## License -MIT — see [LICENSE](./LICENSE). +MIT diff --git a/plugins/sentry/agents/sentry-debug-agent.md b/plugins/sentry/agents/sentry-debug-agent.md deleted file mode 100644 index 0cc4e9c..0000000 --- a/plugins/sentry/agents/sentry-debug-agent.md +++ /dev/null @@ -1,128 +0,0 @@ -# Sentry Debug Agent - -You are a specialized debugging agent that helps developers triage, investigate, and resolve errors reported by Sentry. You have deep knowledge of Sentry's event model, SDK behavior, and common error patterns across JavaScript/TypeScript, Python, Go, and Java ecosystems. - -## Capabilities - -1. **Error Triage** — Prioritize and categorize Sentry issues by impact, frequency, and affected users. -2. **Stack Trace Analysis** — Read and interpret stack traces, including minified/source-mapped frames, to locate the root cause. -3. **Breadcrumb Analysis** — Reconstruct the sequence of events (user actions, network requests, console logs) leading up to an error. -4. **Context Inspection** — Examine tags, extra data, user context, device context, and custom contexts attached to events. -5. **Fix Suggestions** — Propose concrete code fixes based on recognized error patterns. -6. **Performance Issue Investigation** — Analyze slow transactions, N+1 queries, and other performance issues surfaced by Sentry. - -## Workflow - -### Step 1: Gather Information - -When a user asks for help with a Sentry issue, collect the following: - -- **Issue title and error message** — The exception type and message string. -- **Stack trace** — Full stack trace with file names, line numbers, and function names. -- **Breadcrumbs** — The trail of events before the error occurred. -- **Tags and context** — Environment, release, user info, custom tags. -- **Frequency and impact** — How many events, how many users affected, first/last seen timestamps. -- **Related code** — The source files referenced in the stack trace. - -### Step 2: Analyze the Error - -1. **Identify the exception type** — Determine whether it is a runtime error, unhandled promise rejection, network failure, assertion error, etc. -2. **Trace the call chain** — Walk the stack trace from the throw site upward to understand how execution reached the failure point. -3. **Check breadcrumbs for context** — Look for recent API calls, user interactions, state changes, or navigation events that set up the failure conditions. -4. **Examine tags and context** — Check environment, release, browser/device, and custom tags for patterns (e.g., error only in Safari, only for free-tier users, only after a specific release). -5. **Identify patterns** — Compare against common error patterns: - - `TypeError: Cannot read properties of undefined` → missing null check or race condition. - - `ChunkLoadError` → stale deployment, missing code-split chunk. - - `NetworkError` / `Failed to fetch` → API downtime, CORS misconfiguration, client offline. - - `RangeError: Maximum call stack size exceeded` → infinite recursion. - - `TimeoutError` → slow upstream dependency. - - `SyntaxError: Unexpected token` → malformed API response, HTML error page returned instead of JSON. - -### Step 3: Suggest a Fix - -Provide a concrete, actionable fix that includes: - -1. **Root cause explanation** — A clear, concise description of why the error occurs. -2. **Code change** — The specific code change needed, with before/after examples. -3. **Prevention strategy** — How to prevent similar errors in the future (input validation, error boundaries, retry logic, type guards, etc.). -4. **Verification steps** — How to confirm the fix works (unit test, manual reproduction, Sentry issue resolution). - -### Step 4: Follow Up - -- Recommend adding a **test case** that covers the failure scenario. -- Suggest adding **Sentry context** (breadcrumbs, tags) if the error was hard to diagnose due to missing information. -- Propose **alert rules** if the error class is critical and should trigger immediate notification. - -## Common Error Patterns & Resolutions - -### Unhandled Promise Rejection - -**Pattern:** `UnhandledRejection: ...` with no meaningful stack trace. - -**Cause:** An async function throws, but the caller does not `await` or `.catch()` the promise. - -**Fix:** -```ts -// Before — fire and forget -sendAnalyticsEvent(data); - -// After — handle the error -sendAnalyticsEvent(data).catch((err) => { - Sentry.captureException(err, { tags: { module: "analytics" } }); -}); -``` - -### Stale Chunk / ChunkLoadError - -**Pattern:** `ChunkLoadError: Loading chunk X failed` after a deployment. - -**Cause:** The user's browser has cached an old HTML page that references chunk filenames from the previous build. Those files no longer exist on the server. - -**Fix:** -```ts -// Add a global error handler that reloads on chunk errors -window.addEventListener("error", (event) => { - if (/Loading chunk .* failed/.test(event.message)) { - window.location.reload(); - } -}); -``` - -Also consider content-hash-based filenames and proper cache-busting headers. - -### N+1 Query Detection - -**Pattern:** Sentry Performance shows a transaction with hundreds of near-identical `db.query` spans. - -**Cause:** A loop that queries the database individually for each item instead of batching. - -**Fix:** -```ts -// Before — N+1 -for (const userId of userIds) { - const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]); - results.push(user); -} - -// After — single query -const users = await db.query( - "SELECT * FROM users WHERE id IN (?)", - [userIds] -); -``` - -### Missing Source Maps - -**Pattern:** Stack traces show minified code (single-letter variables, `a.js:1:23456`). - -**Resolution:** Ensure source maps are uploaded to Sentry during the build step and that the `release` value in `Sentry.init()` matches the release name used during upload. - -## Response Format - -When responding to a Sentry debugging request, structure your answer as: - -1. **Summary** — One-sentence description of the root cause. -2. **Analysis** — Detailed walkthrough of the stack trace, breadcrumbs, and context. -3. **Fix** — Code changes with before/after examples. -4. **Prevention** — How to avoid recurrence. -5. **Verification** — How to confirm the fix resolves the issue. diff --git a/plugins/sentry/hooks/hooks.json b/plugins/sentry/hooks/hooks.json deleted file mode 100644 index 42beed3..0000000 --- a/plugins/sentry/hooks/hooks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "id": "sentry-create-release", - "name": "Create Sentry Release", - "description": "Creates a new Sentry release, associates commits, uploads source maps, and records a deployment before the application is deployed.", - "trigger": "pre-deploy", - "script": "./scripts/sentry-release.sh", - "env": { - "SENTRY_AUTH_TOKEN": "${SENTRY_AUTH_TOKEN}", - "SENTRY_ORG": "${SENTRY_ORG}", - "SENTRY_PROJECT": "${SENTRY_PROJECT}", - "SENTRY_ENVIRONMENT": "${SENTRY_ENVIRONMENT:-production}" - }, - "timeout": 300, - "failOnError": true, - "conditions": { - "branches": ["main", "master", "release/*"], - "events": ["push", "workflow_dispatch"] - } - } - ] -} diff --git a/plugins/sentry/rules/sentry-integration.mdc b/plugins/sentry/rules/sentry-integration.mdc deleted file mode 100644 index dda1f56..0000000 --- a/plugins/sentry/rules/sentry-integration.mdc +++ /dev/null @@ -1,207 +0,0 @@ ---- -description: Best practices for integrating and using the Sentry SDK for error monitoring, context enrichment, and data hygiene. -globs: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" - - "**/*.jsx" -alwaysApply: false ---- - -# Sentry Integration Best Practices - -## Initialization - -- Initialize Sentry as early as possible in the application lifecycle — before any other imports or framework bootstrapping. - - In Node.js: call `Sentry.init()` at the very top of your entry file, before importing routes or middleware. - - In Next.js: use `sentry.client.config.ts` and `sentry.server.config.ts` (or the `instrumentation.ts` hook) so initialization runs before page/route code. - - In React SPAs: call `Sentry.init()` before `ReactDOM.createRoot()`. - -```ts -// Example: Node.js entry point -import * as Sentry from "@sentry/node"; - -Sentry.init({ - dsn: process.env.SENTRY_DSN, - environment: process.env.NODE_ENV, - release: process.env.SENTRY_RELEASE, - tracesSampleRate: process.env.NODE_ENV === "production" ? 0.2 : 1.0, -}); - -// All other imports AFTER Sentry.init() -import express from "express"; -``` - -## DSN Configuration - -- **Never** hard-code the Sentry DSN in source code. Always read it from an environment variable (`SENTRY_DSN`). -- Keep DSNs different per environment (development, staging, production) to avoid polluting production data with local errors. -- Validate that `SENTRY_DSN` is set at startup; log a warning if it is missing so issues are caught early. - -## Sample Rates - -- Set `tracesSampleRate` to a value appropriate for the environment: - - **Development / staging**: `1.0` (capture everything for full visibility). - - **Production**: `0.1`–`0.3` for most workloads; adjust based on traffic volume and budget. -- Use `tracesSampler` for dynamic sampling when you need different rates per transaction: - -```ts -Sentry.init({ - tracesSampler: (samplingContext) => { - if (samplingContext.name?.includes("/health")) return 0; - if (samplingContext.name?.includes("/api/checkout")) return 1.0; - return 0.2; - }, -}); -``` - -## Breadcrumbs - -- Add custom breadcrumbs before critical operations to provide extra debugging context: - -```ts -Sentry.addBreadcrumb({ - category: "auth", - message: `User ${userId} attempted login`, - level: "info", - data: { method: "oauth", provider: "github" }, -}); -``` - -- Avoid logging sensitive data (passwords, tokens, PII) in breadcrumb messages or data fields. -- Sentry auto-captures console logs, XHR/fetch, and DOM clicks as breadcrumbs — disable categories you do not need via `beforeBreadcrumb`. - -## User Context - -- Set user context immediately after authentication so every subsequent event is tied to the user: - -```ts -Sentry.setUser({ - id: user.id, - email: user.email, - username: user.username, -}); -``` - -- Clear user context on logout: `Sentry.setUser(null)`. -- Never include passwords, tokens, or full credit card numbers in user context. - -## Capturing Errors with Context - -- When catching errors, always forward them to Sentry with additional context: - -```ts -try { - await processOrder(orderId); -} catch (error) { - Sentry.captureException(error, { - tags: { module: "orders", orderId }, - extra: { payload: sanitizedPayload }, - level: "error", - }); - throw error; // re-throw if the caller needs to handle it -} -``` - -- Use **tags** for indexed, searchable key-value pairs (low cardinality). -- Use **extra** for arbitrary debug data that does not need to be searchable. -- Use **contexts** for structured data blocks (e.g., device info, order details). - -## Scoped Context with `Sentry.withScope` - -- Use `Sentry.withScope` when you need temporary context that should not leak to other events: - -```ts -Sentry.withScope((scope) => { - scope.setTag("transaction_type", "refund"); - scope.setExtra("refundDetails", { amount, reason }); - scope.setLevel("warning"); - Sentry.captureMessage("Refund processed with warnings"); -}); -``` - -## Source Maps - -- Upload source maps as part of your build/deploy pipeline so stack traces in Sentry show original source code. -- Use `@sentry/webpack-plugin`, `@sentry/vite-plugin`, or `sentry-cli` to upload source maps tied to a release. -- **Do not** serve source maps publicly; upload them to Sentry and exclude them from the production bundle. - -```js -// Example: Vite plugin -import { sentryVitePlugin } from "@sentry/vite-plugin"; - -export default defineConfig({ - build: { sourcemap: true }, - plugins: [ - sentryVitePlugin({ - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - authToken: process.env.SENTRY_AUTH_TOKEN, - }), - ], -}); -``` - -## Filtering Sensitive Data with `beforeSend` - -- Use the `beforeSend` hook to scrub PII, secrets, or noisy errors before they leave the client: - -```ts -Sentry.init({ - beforeSend(event) { - // Drop events from browser extensions - if (event.exception?.values?.some((e) => - e.stacktrace?.frames?.some((f) => f.filename?.includes("extensions://")) - )) { - return null; - } - - // Scrub sensitive headers - if (event.request?.headers) { - delete event.request.headers["Authorization"]; - delete event.request.headers["Cookie"]; - } - - return event; - }, -}); -``` - -- Use `beforeSendTransaction` to filter or modify transaction events similarly. - -## React Error Boundaries - -- Wrap your application (or key subtrees) with `Sentry.ErrorBoundary` to catch rendering errors: - -```tsx -import * as Sentry from "@sentry/react"; - -function App() { - return ( - <Sentry.ErrorBoundary - fallback={({ error, resetError }) => ( - <ErrorFallback error={error} onReset={resetError} /> - )} - showDialog - > - <MainApp /> - </Sentry.ErrorBoundary> - ); -} -``` - -- For granular control, create custom error boundaries that call `Sentry.captureException` in `componentDidCatch`. - -## Environment and Release Tags - -- Always set `environment` (e.g., `"production"`, `"staging"`, `"development"`) in `Sentry.init()` to segment events. -- Always set `release` to a unique identifier (git SHA or semantic version) so you can correlate errors with deployments: - -```ts -Sentry.init({ - environment: process.env.NODE_ENV, - release: `myapp@${process.env.COMMIT_SHA}`, -}); -``` - -- Use Sentry Releases to track which deploy introduced a regression and to associate commits and source maps. diff --git a/plugins/sentry/rules/sentry-performance.mdc b/plugins/sentry/rules/sentry-performance.mdc deleted file mode 100644 index a12bc0b..0000000 --- a/plugins/sentry/rules/sentry-performance.mdc +++ /dev/null @@ -1,172 +0,0 @@ ---- -description: Best practices for Sentry performance monitoring — custom transactions, spans, sampling, profiling, and web vitals tracking. -globs: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" -alwaysApply: false ---- - -# Sentry Performance Monitoring Best Practices - -## Custom Transactions for Business Operations - -- Create custom transactions around important business flows so they appear as discrete units in the Performance dashboard: - -```ts -const transaction = Sentry.startTransaction({ - op: "checkout", - name: "POST /api/checkout", - data: { cartItems: cart.items.length }, -}); - -Sentry.getCurrentHub().configureScope((scope) => { - scope.setSpan(transaction); -}); - -try { - await validateCart(cart); - await chargePayment(cart); - await createOrder(cart); - transaction.setStatus("ok"); -} catch (error) { - transaction.setStatus("internal_error"); - Sentry.captureException(error); - throw error; -} finally { - transaction.finish(); -} -``` - -> **Note:** In SDK v8+ the recommended API uses `Sentry.startSpan` / `Sentry.startInactiveSpan` which automatically manage the scope. Prefer the newer API when available: - -```ts -await Sentry.startSpan( - { op: "checkout", name: "POST /api/checkout" }, - async (span) => { - await validateCart(cart); - await chargePayment(cart); - await createOrder(cart); - } -); -``` - -## Adding Child Spans for Detailed Timing - -- Break transactions into child spans to identify exactly where time is spent: - -```ts -await Sentry.startSpan( - { op: "checkout", name: "POST /api/checkout" }, - async () => { - await Sentry.startSpan( - { op: "db.query", name: "validate-cart" }, - async () => { - await validateCart(cart); - } - ); - - await Sentry.startSpan( - { op: "http.client", name: "charge-payment-gateway" }, - async () => { - await chargePayment(cart); - } - ); - - await Sentry.startSpan( - { op: "db.insert", name: "create-order-record" }, - async () => { - await createOrder(cart); - } - ); - } -); -``` - -- Use descriptive `op` values that follow Sentry conventions: `db.query`, `http.client`, `http.server`, `grpc.client`, `queue.process`, `cache.get`, `serialize`, `template.render`, etc. - -## Meaningful Transaction Names - -- Use a consistent naming convention for transactions so they aggregate correctly in the Performance dashboard: - - **HTTP endpoints**: `GET /api/users/:id` (parameterized, not `/api/users/123`). - - **Background jobs**: `job.send-welcome-email`, `job.generate-report`. - - **Frontend pages**: `pageload /dashboard`, `navigation /settings`. -- Avoid high-cardinality transaction names (e.g., embedding UUIDs or query strings) — they fragment your data and make it hard to spot trends. - -## Traces Sample Rate Configuration - -- **Never** set `tracesSampleRate: 1.0` in production unless traffic is extremely low. Sampling 100 % of traces at scale will: - - Exhaust your Sentry quota rapidly. - - Add unnecessary overhead to every request. -- Recommended production ranges: - - **Low traffic** (< 10 k requests/day): `0.5`–`1.0` - - **Medium traffic** (10 k–1 M requests/day): `0.1`–`0.3` - - **High traffic** (> 1 M requests/day): `0.01`–`0.1` -- Use `tracesSampler` for dynamic, context-aware sampling: - -```ts -Sentry.init({ - tracesSampler: (ctx) => { - // Always trace critical paths - if (ctx.name?.startsWith("POST /api/payment")) return 1.0; - // Drop noisy health checks - if (ctx.name?.includes("/healthz")) return 0; - // Default - return 0.2; - }, -}); -``` - -## Profiling Slow Endpoints - -- Enable Sentry Profiling to capture CPU flame graphs alongside traces: - -```ts -import * as Sentry from "@sentry/node"; -import { nodeProfilingIntegration } from "@sentry/profiling-node"; - -Sentry.init({ - dsn: process.env.SENTRY_DSN, - integrations: [nodeProfilingIntegration()], - tracesSampleRate: 0.2, - profilesSampleRate: 0.1, // profile 10 % of sampled transactions -}); -``` - -- Profiling is most valuable for endpoints with high p95/p99 latency — focus sampling budget there using `profilesSampler`. -- Use the Profiling flamechart in Sentry to identify hot functions, excessive allocations, or synchronous bottlenecks. - -## Tracking Web Vitals - -- The Sentry Browser SDK automatically captures Core Web Vitals (LCP, FID/INP, CLS) when `BrowserTracing` integration is enabled: - -```ts -import * as Sentry from "@sentry/react"; - -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - integrations: [Sentry.browserTracingIntegration()], - tracesSampleRate: 0.3, -}); -``` - -- Monitor Web Vitals in the Sentry Performance → Web Vitals dashboard to spot regressions per release. -- Add custom web-vital measurements for metrics Sentry does not capture automatically: - -```ts -Sentry.startSpan({ name: "pageload /dashboard", op: "pageload" }, (span) => { - // Custom measurement - span.setAttribute("custom.time_to_interactive", ttiMs); -}); -``` - -## Best Practices Summary - -| Practice | Rationale | -|---|---| -| Use parameterized transaction names | Prevents cardinality explosion and enables aggregation | -| Add child spans for I/O calls | Pinpoints latency sources within a transaction | -| Set `tracesSampleRate` < 1.0 in prod | Avoids quota exhaustion and runtime overhead | -| Enable profiling for slow paths | Identifies CPU-level bottlenecks that spans alone cannot reveal | -| Monitor Web Vitals per release | Catches frontend perf regressions before users report them | -| Use `tracesSampler` over static rate | Allows per-route control and keeps critical paths fully sampled | diff --git a/plugins/sentry/scripts/sentry-release.sh b/plugins/sentry/scripts/sentry-release.sh deleted file mode 100755 index 4bac4de..0000000 --- a/plugins/sentry/scripts/sentry-release.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# -# sentry-release.sh — Create a Sentry release, associate commits, upload -# source maps, finalize the release, and record a deployment. -# -# Required environment variables: -# SENTRY_AUTH_TOKEN — Sentry API auth token -# SENTRY_ORG — Sentry organization slug -# SENTRY_PROJECT — Sentry project slug -# -# Optional environment variables: -# SENTRY_ENVIRONMENT — Deployment environment (default: production) -# SENTRY_RELEASE — Release version (default: short git SHA) -# SOURCE_MAP_PATH — Path to source map directory (default: ./dist) -# URL_PREFIX — URL prefix for source maps (default: ~/) -# -set -euo pipefail - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- -SENTRY_ENVIRONMENT="${SENTRY_ENVIRONMENT:-production}" -SENTRY_RELEASE="${SENTRY_RELEASE:-$(git rev-parse --short HEAD)}" -SOURCE_MAP_PATH="${SOURCE_MAP_PATH:-./dist}" -URL_PREFIX="${URL_PREFIX:-~/}" - -# --------------------------------------------------------------------------- -# Validation -# --------------------------------------------------------------------------- -missing_vars=() -[ -z "${SENTRY_AUTH_TOKEN:-}" ] && missing_vars+=("SENTRY_AUTH_TOKEN") -[ -z "${SENTRY_ORG:-}" ] && missing_vars+=("SENTRY_ORG") -[ -z "${SENTRY_PROJECT:-}" ] && missing_vars+=("SENTRY_PROJECT") - -if [ ${#missing_vars[@]} -gt 0 ]; then - echo "ERROR: Missing required environment variables: ${missing_vars[*]}" >&2 - exit 1 -fi - -# --------------------------------------------------------------------------- -# Ensure sentry-cli is available -# --------------------------------------------------------------------------- -if ! command -v sentry-cli &>/dev/null; then - echo "INFO: sentry-cli not found, installing via npm..." - npm install -g @sentry/cli -fi - -echo "=============================================" -echo " Sentry Release" -echo "=============================================" -echo " Organization : ${SENTRY_ORG}" -echo " Project : ${SENTRY_PROJECT}" -echo " Release : ${SENTRY_RELEASE}" -echo " Environment : ${SENTRY_ENVIRONMENT}" -echo " Source Maps : ${SOURCE_MAP_PATH}" -echo "=============================================" - -# --------------------------------------------------------------------------- -# Step 1: Create the release -# --------------------------------------------------------------------------- -echo "" -echo ">>> Creating release ${SENTRY_RELEASE}..." -sentry-cli releases new "${SENTRY_RELEASE}" \ - --org "${SENTRY_ORG}" \ - --project "${SENTRY_PROJECT}" - -# --------------------------------------------------------------------------- -# Step 2: Associate commits -# --------------------------------------------------------------------------- -echo "" -echo ">>> Associating commits..." -sentry-cli releases set-commits "${SENTRY_RELEASE}" \ - --org "${SENTRY_ORG}" \ - --auto || echo "WARN: Could not associate commits (missing repo integration?)" - -# --------------------------------------------------------------------------- -# Step 3: Upload source maps -# --------------------------------------------------------------------------- -if [ -d "${SOURCE_MAP_PATH}" ]; then - echo "" - echo ">>> Uploading source maps from ${SOURCE_MAP_PATH}..." - sentry-cli releases files "${SENTRY_RELEASE}" upload-sourcemaps \ - "${SOURCE_MAP_PATH}" \ - --org "${SENTRY_ORG}" \ - --project "${SENTRY_PROJECT}" \ - --url-prefix "${URL_PREFIX}" \ - --rewrite -else - echo "" - echo "WARN: Source map directory '${SOURCE_MAP_PATH}' does not exist — skipping upload." -fi - -# --------------------------------------------------------------------------- -# Step 4: Finalize the release -# --------------------------------------------------------------------------- -echo "" -echo ">>> Finalizing release ${SENTRY_RELEASE}..." -sentry-cli releases finalize "${SENTRY_RELEASE}" \ - --org "${SENTRY_ORG}" - -# --------------------------------------------------------------------------- -# Step 5: Record the deployment -# --------------------------------------------------------------------------- -echo "" -echo ">>> Recording deployment to ${SENTRY_ENVIRONMENT}..." -sentry-cli releases deploys "${SENTRY_RELEASE}" new \ - --org "${SENTRY_ORG}" \ - --env "${SENTRY_ENVIRONMENT}" \ - --name "deploy-$(date +%s)" - -echo "" -echo "=============================================" -echo " Sentry release ${SENTRY_RELEASE} complete!" -echo "=============================================" diff --git a/plugins/slack/.cursor/plugin.json b/plugins/slack/.cursor/plugin.json index b3ee7bc..7c38693 100644 --- a/plugins/slack/.cursor/plugin.json +++ b/plugins/slack/.cursor/plugin.json @@ -1,7 +1,7 @@ { "name": "slack", "version": "1.0.0", - "description": "Cursor plugin for Slack — Bolt framework, Block Kit, Events API, and Slack app development", + "description": "Cursor plugin for Slack \u2014 Bolt framework, Block Kit, Events API, and Slack app development", "author": { "name": "Cursor", "email": "plugins@cursor.com" @@ -9,13 +9,19 @@ "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["slack", "bolt", "block-kit", "chatbot", "integrations"], + "keywords": [ + "slack", + "bolt", + "block-kit", + "chatbot", + "integrations" + ], "category": "saas", - "tags": ["messaging", "chatbot", "collaboration"], - "agents": "./agents/", + "tags": [ + "messaging", + "chatbot", + "collaboration" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/slack/CHANGELOG.md b/plugins/slack/CHANGELOG.md index 0e7a8f8..5f2859d 100644 --- a/plugins/slack/CHANGELOG.md +++ b/plugins/slack/CHANGELOG.md @@ -1,33 +1,13 @@ # Changelog -All notable changes to the Slack plugin for Cursor will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-08 +## [Unreleased] -### Added - -- **Plugin manifest** (`.cursor/plugin.json`) — Metadata, entry points, and configuration for the Slack plugin. -- **Rules** - - `rules/slack-bolt.mdc` — Best practices for the Bolt framework: using the Bolt SDK over raw API calls, immediate ack() acknowledgment, Block Kit for rich messages, request signature validation, rate-limit retry handling, Socket Mode for development, minimal OAuth scopes, secure token storage, app_mention vs message events, modals for complex interactions, and API response pagination. - - `rules/slack-block-kit.mdc` — Best practices for Block Kit: using Block Kit Builder for design, concise message layout, proper block types (header, section, divider, actions, context, image, input, rich_text), interactive component handling with unique action_ids, composition objects (plain_text vs mrkdwn), block limit validation (50 blocks per message), mrkdwn formatting syntax, and modal submission handling with validation and view updates. -- **Agents** - - `agents/slack-app-agent.md` — Specialized agent for Slack app development: app architecture design, Bolt framework code generation, Block Kit message and modal creation, event and webhook handling, slash command implementation, and debugging common Slack API errors. -- **Skills** - - `skills/create-slack-bot/SKILL.md` — Step-by-step guide for creating a Slack bot with the Bolt framework, including Slack App dashboard setup, project scaffolding, event listeners (app_mention, message, reaction_added, app_home_opened), interactive buttons, App Home tab, and deployment options (Railway/Render, AWS Lambda). - - `skills/setup-slash-commands/SKILL.md` — Guide for implementing slash commands with argument parsing, subcommand routing, modal triggers for data entry, deferred responses for long operations, modal submission handling with input validation, and project structure patterns. -- **Hooks** (`hooks/hooks.json`) - - Pre-commit hook to validate Bolt listeners call ack() and detect hard-coded Slack tokens. - - Pre-commit hook to validate Block Kit JSON syntax and check the 50-block limit. - - Post-save hook to auto-format Slack app source files with Prettier. -- **MCP server** (`mcp.json`) — Configuration for `@anthropic/mcp-server-slack` providing AI-accessible Slack API operations: channel listing, message posting, threading, reactions, user profiles, and message search. -- **Scripts** - - `scripts/setup-slack-app.sh` — Environment validation script checking Node.js version, npm availability, project structure, @slack/bolt dependency, environment variables, .env security, token format, and app dashboard configuration. -- **Documentation** - - `README.md` — Plugin overview, setup guide, usage instructions, and project structure. - - `CHANGELOG.md` — This changelog. - - `LICENSE` — MIT License. +## [1.0.0] - 2026-02-07 -[1.0.0]: https://github.com/cursor/service-plugin-generation/releases/tag/slack-plugin-v1.0.0 +### Added +- Initial plugin with skills and MCP server configuration diff --git a/plugins/slack/README.md b/plugins/slack/README.md index 7e6d35f..2785651 100644 --- a/plugins/slack/README.md +++ b/plugins/slack/README.md @@ -1,167 +1,28 @@ -# Slack Plugin for Cursor +# Slack Plugin -A comprehensive Cursor plugin for building Slack apps, bots, and integrations — covering the Bolt framework, Block Kit, Events API, slash commands, interactive components, and modals. +Cursor plugin for Slack — Bolt framework, Block Kit, Events API, and Slack app development. -## Overview - -This plugin provides rules, agents, skills, hooks, and MCP server configuration to help developers build Slack apps effectively from Cursor. - -## Contents - -### Rules - -| File | Scope | Description | -|------|-------|-------------| -| `rules/slack-bolt.mdc` | `**/*.ts`, `**/*.js` | Best practices for the Bolt framework: ack() handling, event listeners, Socket Mode, OAuth scopes, token security, rate limiting, pagination, modals | -| `rules/slack-block-kit.mdc` | `**/*.ts`, `**/*.js`, `**/*.json` | Best practices for Block Kit: block types, composition objects, interactive components, mrkdwn formatting, block limits, modal submissions | - -### Agents - -| File | Description | -|------|-------------| -| `agents/slack-app-agent.md` | Specialized agent for designing and building Slack apps — event handling, slash commands, interactive components, modals, Block Kit, and deployment | - -### Skills - -| Skill | Trigger | Description | -|-------|---------|-------------| -| `skills/create-slack-bot/SKILL.md` | Creating a new Slack bot | End-to-end guide for building a Bolt bot with event listeners, Block Kit messages, App Home, and deployment options | -| `skills/setup-slash-commands/SKILL.md` | Implementing slash commands | Slash command handlers with argument parsing, subcommand routing, modal triggers, and deferred responses | - -### Hooks - -| Hook | Event | Description | -|------|-------|-------------| -| `validate-slack-bolt-listeners` | `pre-commit` | Checks that Bolt listeners call ack(), detects hard-coded Slack tokens | -| `validate-block-kit-json` | `pre-commit` | Validates Block Kit JSON syntax and checks the 50-block limit | -| `format-slack-source` | `post-save` | Auto-formats Slack app source files with Prettier on save | - -### MCP Server - -The `mcp.json` configures the `@anthropic/mcp-server-slack` MCP server, providing AI-accessible Slack API operations: - -- Channel listing and history -- Message posting and threading -- Reaction management -- User lookup and profile access -- Message search - -### Scripts - -| Script | Description | -|--------|-------------| -| `scripts/setup-slack-app.sh` | Environment validator checking Node.js, dependencies, tokens, .env security, and token format | - -## Setup - -### 1. Install the Plugin - -Copy or symlink the `plugins/slack/` directory into your Cursor workspace. - -### 2. Configure the MCP Server - -Set the required environment variables for the MCP server: +## Installation ```bash -export SLACK_BOT_TOKEN="xoxb-your-bot-token" -export SLACK_TEAM_ID="T0123456789" +agent install slack ``` -### 3. Create a Slack App - -If you don't have a Slack app yet, create one at [https://api.slack.com/apps](https://api.slack.com/apps): +## Components -1. Click **Create New App** → **From scratch**. -2. Add Bot Token Scopes under **OAuth & Permissions**: - - `chat:write` — Post messages - - `app_mentions:read` — Receive @mentions - - `commands` — Handle slash commands -3. Enable **Socket Mode** and create an App-Level Token with `connections:write`. -4. Enable **Event Subscriptions** and subscribe to `app_mention`. -5. Install the app to your workspace. - -### 4. Set Environment Variables - -```bash -# .env (for local development) -SLACK_BOT_TOKEN=xoxb-your-bot-token -SLACK_SIGNING_SECRET=your-signing-secret -SLACK_APP_TOKEN=xapp-1-your-app-level-token -``` - -### 5. Validate Your Setup - -```bash -chmod +x plugins/slack/scripts/setup-slack-app.sh -./plugins/slack/scripts/setup-slack-app.sh -``` - -## Usage - -### With Cursor AI - -The plugin activates automatically when you work with relevant files: - -- **Writing Bolt app code** — Slack Bolt rules activate when editing `.ts` or `.js` files, providing guidance on ack() handling, event listeners, and token security. -- **Designing Block Kit messages** — Block Kit rules activate for `.ts`, `.js`, and `.json` files, enforcing block limits and formatting best practices. -- **Building a Slack app** — The Slack App Agent provides expert guidance on architecture, interaction patterns, and deployment. -- **Creating a bot** — Use the "Create a Slack Bot" skill for a step-by-step walkthrough. -- **Adding slash commands** — Use the "Set Up Slash Commands" skill for command handlers with subcommand routing. - -### Running the Setup Validator - -```bash -# Validate the current directory -./plugins/slack/scripts/setup-slack-app.sh - -# Validate a specific project -./plugins/slack/scripts/setup-slack-app.sh path/to/slack-app -``` - -The validator checks: -1. Node.js version (>= 18) -2. npm availability -3. Project structure (package.json, tsconfig.json, src/) -4. Required dependencies (`@slack/bolt`) -5. Environment variables (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_APP_TOKEN) -6. .env file security (.gitignore inclusion) -7. Token format validation (xoxb-, xapp- prefixes) -8. App dashboard configuration checklist +### Skills -## Project Structure +| Skill | Description | +|:------|:------------| +| `create-slack-bot` | End-to-end bot setup with event listeners, interactive components, and deployment | +| `setup-slash-commands` | Slash command handlers with subcommand routing, modals, and deferred responses | -``` -plugins/slack/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── slack-app-agent.md # Slack app development agent -├── rules/ -│ ├── slack-bolt.mdc # Bolt framework best practices -│ └── slack-block-kit.mdc # Block Kit best practices -├── skills/ -│ ├── create-slack-bot/ -│ │ └── SKILL.md # Bot creation guide -│ └── setup-slash-commands/ -│ └── SKILL.md # Slash command setup guide -├── hooks/ -│ └── hooks.json # Pre-commit and post-save hooks -├── scripts/ -│ └── setup-slack-app.sh # Environment validation script -├── extensions/ # Extension point (reserved) -├── mcp.json # MCP server configuration -├── README.md # This file -├── CHANGELOG.md # Version history -└── LICENSE # MIT License -``` +### MCP Server -## Contributing +Provides Slack API access via `@anthropic/mcp-server-slack`. -1. Fork the repository. -2. Create a feature branch. -3. Make your changes and add tests where applicable. -4. Submit a pull request with a clear description. +Requires `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` environment variables. ## License -MIT License — see [LICENSE](LICENSE) for details. +MIT diff --git a/plugins/slack/agents/slack-app-agent.md b/plugins/slack/agents/slack-app-agent.md deleted file mode 100644 index 0397a7c..0000000 --- a/plugins/slack/agents/slack-app-agent.md +++ /dev/null @@ -1,124 +0,0 @@ -# Slack App Development Agent - -You are a specialized agent for Slack app development — covering the Bolt framework, Block Kit, Events API, slash commands, interactive components, and modals. Your purpose is to help developers design, build, test, and deploy Slack apps and integrations. - -## Identity - -- **Name**: Slack App Development Agent -- **Expertise**: Slack Bolt SDK (Node.js, Python), Block Kit, Events API, Web API, OAuth, Socket Mode, slash commands, interactive components, modals, workflows, and app distribution. -- **Tone**: Concise, practical, and focused on working code. Provide copy-paste-ready examples first, then explain trade-offs and alternatives. - -## Responsibilities - -### 1. Design Slack Apps - -Help users plan the architecture and user experience of their Slack app before writing code. - -- Clarify the app's purpose: Is it a bot, a slash command tool, a workflow, or a notification integration? -- Recommend the right interaction model: conversational (message events), command-driven (slash commands), form-based (modals), or passive (incoming webhooks). -- Advise on scopes and permissions — request only what the app needs. -- Guide users on choosing between Socket Mode (development / firewalled) and HTTP mode (production). -- Suggest the right Slack features for the use case (App Home, unfurling, workflow steps, etc.). - -### 2. Build with the Bolt Framework - -Generate production-ready Bolt app code for any interaction type. - -- Scaffold a new Bolt app with proper project structure, TypeScript configuration, and environment variable management. -- Implement event listeners (`app_mention`, `message`, `reaction_added`, `member_joined_channel`, `app_home_opened`, etc.). -- Implement slash command handlers with input parsing and response formatting. -- Build interactive component flows: buttons, selects, overflow menus, date pickers, and their action handlers. -- Create multi-step modal workflows with view submissions, updates, and pushes. -- Set up middleware for logging, authentication, and feature flags. -- Configure OAuth for multi-workspace distribution with `InstallationStore`. - -### 3. Design Block Kit Messages and Modals - -Create rich, accessible, interactive messages and modals using Block Kit. - -- Translate user requirements into Block Kit JSON payloads. -- Use the right block types (`header`, `section`, `actions`, `context`, `divider`, `input`, `image`). -- Build forms in modals with proper input validation and error handling. -- Handle interactive component callbacks with state management. -- Respect block limits (50 blocks per message, 25 elements per actions block, etc.). -- Ensure accessibility: provide `alt_text` for images, `text` fallback for block messages, meaningful button labels. - -### 4. Handle Events and Webhooks - -Set up event subscriptions and process incoming payloads reliably. - -- Configure Event API subscriptions in the Slack App dashboard. -- Implement event listeners with proper `ack()` handling. -- Distinguish between `app_mention`, `message`, and `message.channels` events. -- Handle event retries (Slack resends events if ack is not received within 3 seconds — deduplicate by `event_id`). -- Set up and verify webhook endpoints with signature validation. -- Process `url_verification` challenge events during app setup. - -### 5. Implement Slash Commands - -Build slash commands with rich responses, input parsing, and error handling. - -- Parse command text with argument extraction and subcommand routing. -- Respond with ephemeral messages (visible only to the invoker) or in-channel messages. -- Open modals from slash commands using `trigger_id`. -- Handle deferred responses for long-running operations using `response_url`. -- Validate user permissions before executing commands. - -### 6. Debug and Troubleshoot - -Diagnose common Slack app issues from error messages, logs, and API responses. - -- Identify and resolve `dispatch_failed`, `expired_trigger_id`, `channel_not_found`, `not_in_channel`, and `missing_scope` errors. -- Debug OAuth installation failures and token refresh issues. -- Trace event delivery problems using the Slack App dashboard's Event Subscriptions logs. -- Diagnose rate limiting (HTTP 429) and recommend mitigation strategies. -- Fix Block Kit validation errors by identifying which block or element violates constraints. - -## Process - -When a user asks for help: - -1. **Understand the goal** — Determine whether the user needs a new app, a new feature in an existing app, or help fixing a problem. Ask clarifying questions only when critical context is missing. -2. **Recommend an approach** — Briefly explain the Slack features, APIs, and interaction patterns that fit the use case. -3. **Write the code** — Produce complete, working Bolt code following the rules in `slack-bolt.mdc` and `slack-block-kit.mdc`. Include type annotations, error handling, and `ack()` calls. -4. **Explain decisions** — Note security considerations (scopes, token storage, signature verification), UX trade-offs (ephemeral vs. in-channel, modals vs. messages), and scaling implications. -5. **Suggest next steps** — Recommend testing with Socket Mode, deploying to a platform (Railway, Render, AWS Lambda), adding monitoring, or submitting to the Slack App Directory. - -## Output Format - -When generating Bolt app code: - -```typescript -// src/<feature>.ts -// <One-line description> - -import { App } from "@slack/bolt"; - -// Configuration loaded from environment variables -// SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_APP_TOKEN (Socket Mode) - -app.<listener>("<event-or-action>", async ({ ack, body, client, say }) => { - await ack(); - // Implementation -}); -``` - -After the code block, include: - -- **Scopes required** — List the exact OAuth scopes the feature needs. -- **Events to subscribe** — List Event API event types to enable in the app dashboard. -- **Setup steps** — Any Slack App dashboard configuration (slash commands, interactivity URL, event subscriptions). -- **Testing tip** — How to test the feature locally (typically Socket Mode + a test workspace). - -## Constraints - -- Always use `@slack/bolt` — never hand-roll HTTP handlers for Slack events. -- Always call `await ack()` first in every listener. -- Always include `text` fallback on block messages. -- Always validate modal inputs and return `response_action: "errors"` for invalid data. -- Never hard-code tokens, secrets, or team IDs. -- Never request unnecessary OAuth scopes. -- Prefer `mrkdwn` for formatted text; use `plain_text` only where required (buttons, headers, input labels). -- Prefer ephemeral messages for error responses and help text. -- Prefer modals for multi-field data entry over conversational message flows. -- Handle errors gracefully — catch exceptions in listeners and notify the user. diff --git a/plugins/slack/hooks/hooks.json b/plugins/slack/hooks/hooks.json deleted file mode 100644 index 436fd2f..0000000 --- a/plugins/slack/hooks/hooks.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "name": "validate-slack-bolt-listeners", - "event": "pre-commit", - "description": "Validates that Slack Bolt event and action listeners call ack() before performing other operations", - "match": { - "files": [ - "src/**/*.ts", - "src/**/*.js", - "lib/**/*.ts", - "lib/**/*.js" - ] - }, - "steps": [ - { - "name": "check-ack-calls", - "command": "grep -n 'app\\.\\(command\\|action\\|view\\|shortcut\\|options\\)' ${file} | head -20", - "description": "List Bolt listeners to verify ack() is present in handler files", - "onFailure": "ignore" - }, - { - "name": "check-missing-ack", - "command": "python3 -c \"import re, sys; content=open(sys.argv[1]).read(); listeners=re.findall(r'app\\.(command|action|view|shortcut|options)\\([^)]+,\\s*async\\s*\\([^)]*\\)\\s*=>\\s*\\{([^}]{0,500})', content); missing=[l for l in listeners if 'ack()' not in l[1] and 'ack(' not in l[1]]; sys.exit(1) if missing else sys.exit(0)\" ${file}", - "description": "Check that all Bolt listeners call ack() early in the handler body", - "onFailure": "warn" - }, - { - "name": "check-hardcoded-tokens", - "command": "grep -nE '(xoxb-[0-9]|xapp-1-|xoxp-[0-9])' ${file}", - "description": "Detect hard-coded Slack tokens in source files", - "onFailure": "block" - } - ] - }, - { - "name": "validate-block-kit-json", - "event": "pre-commit", - "description": "Validates Block Kit JSON payloads for common structural issues", - "match": { - "files": [ - "**/*.json" - ] - }, - "steps": [ - { - "name": "json-syntax-check", - "command": "python3 -c \"import json, sys; json.load(open(sys.argv[1]))\" ${file}", - "description": "Verify the file is valid JSON", - "onFailure": "block" - }, - { - "name": "check-block-limit", - "command": "python3 -c \"import json, sys; data=json.load(open(sys.argv[1])); blocks=data.get('blocks', []); sys.exit(1) if len(blocks) > 50 else sys.exit(0)\" ${file}", - "description": "Ensure Block Kit payloads do not exceed the 50-block limit", - "onFailure": "warn", - "condition": "grep -q '\"blocks\"' ${file}" - } - ] - }, - { - "name": "format-slack-source", - "event": "post-save", - "description": "Auto-formats Slack app TypeScript and JavaScript files on save", - "match": { - "files": [ - "src/**/*.ts", - "src/**/*.js" - ] - }, - "steps": [ - { - "name": "prettier-format", - "command": "npx prettier --write ${file}", - "description": "Format source code with Prettier for consistent style", - "onFailure": "ignore", - "condition": "command -v npx &> /dev/null" - } - ] - } - ] -} diff --git a/plugins/slack/rules/slack-block-kit.mdc b/plugins/slack/rules/slack-block-kit.mdc deleted file mode 100644 index 2e8994c..0000000 --- a/plugins/slack/rules/slack-block-kit.mdc +++ /dev/null @@ -1,275 +0,0 @@ ---- -description: Best practices for designing and building Slack messages with Block Kit -globs: - - "**/*.ts" - - "**/*.js" - - "**/*.json" -alwaysApply: false ---- - -# Slack Block Kit Best Practices - -## Use Block Kit Builder for Design - -Always prototype messages and modals in the [Block Kit Builder](https://app.slack.com/block-kit-builder) before writing code. The builder provides a live preview across desktop, mobile, and notification contexts, making it easy to catch layout issues early. - -Export the JSON payload from the builder and use it as the starting point for your code. This reduces iteration time and ensures visual fidelity. - -## Keep Messages Concise - -Slack messages should be scannable. Users read messages in a fast-moving stream, so every block must earn its space. - -- Lead with the most important information in a `header` or bold `mrkdwn` text. -- Use `section` fields (side-by-side layout) for key-value data instead of stacking multiple sections. -- Reserve `context` blocks for metadata (timestamps, usernames, source labels) — not primary content. -- Use `divider` blocks sparingly to separate logically distinct sections. - -```typescript -// Good — scannable, key info first -const blocks = [ - { - type: "header", - text: { type: "plain_text", text: "🚨 Incident Alert" }, - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "*Severity:*\nP1 — Critical" }, - { type: "mrkdwn", text: "*Service:*\nPayment API" }, - { type: "mrkdwn", text: "*Started:*\n<!date^1706000000^{date_short} at {time}|2025-01-23>" }, - { type: "mrkdwn", text: "*On-Call:*\n<@U12345678>" }, - ], - }, - { - type: "actions", - elements: [ - { - type: "button", - text: { type: "plain_text", text: "Acknowledge" }, - style: "primary", - action_id: "ack_incident", - }, - { - type: "button", - text: { type: "plain_text", text: "View Dashboard" }, - url: "https://grafana.example.com/d/abc123", - action_id: "view_dashboard", - }, - ], - }, -]; -``` - -## Use the Right Block Types - -Each block type serves a specific purpose. Choose the right one for the content: - -| Block Type | Purpose | Max Per Message | -|-----------|---------|-----------------| -| `header` | Page/section title — renders large, bold | Unlimited (but use sparingly) | -| `section` | Primary content with optional accessory (button, image, overflow) | 50 | -| `divider` | Visual separator between logical sections | 50 | -| `actions` | Row of interactive elements (buttons, selects, date pickers) | 50 (max 25 elements per block) | -| `context` | Supplementary metadata — small text and images | 50 (max 10 elements per block) | -| `image` | Full-width image with alt text | 50 | -| `input` | Form field — only valid inside modals and workflow steps | 50 | -| `rich_text` | Formatted text with lists, quotes, code blocks | 50 | -| `video` | Embedded video player | 50 | - -## Handle Interactive Components Properly - -Every interactive element (`button`, `static_select`, `overflow`, `datepicker`, `timepicker`, `checkboxes`, `radio_buttons`, `multi_select`) must have a unique `action_id`. Register a Bolt listener for each `action_id`. - -```typescript -// Button -{ - type: "button", - text: { type: "plain_text", text: "Approve" }, - style: "primary", - action_id: "approve_request", - value: "req_12345", // Pass context data -} - -// Static select -{ - type: "static_select", - action_id: "select_environment", - placeholder: { type: "plain_text", text: "Choose environment" }, - options: [ - { text: { type: "plain_text", text: "Staging" }, value: "staging" }, - { text: { type: "plain_text", text: "Production" }, value: "production" }, - ], -} -``` - -Corresponding listeners: - -```typescript -app.action("approve_request", async ({ ack, action, say }) => { - await ack(); - const requestId = (action as ButtonAction).value; - await approveRequest(requestId); - await say(`Request ${requestId} approved ✓`); -}); - -app.action("select_environment", async ({ ack, action }) => { - await ack(); - const env = (action as StaticSelectAction).selected_option?.value; - // Process selection -}); -``` - -## Use Composition Objects Correctly - -Block Kit has two text object types. Using the wrong one causes API errors. - -| Type | Supports Formatting | Max Length | Use In | -|------|---------------------|-----------|--------| -| `plain_text` | No (renders literally) | Varies by context | Buttons, headers, placeholders, option labels | -| `mrkdwn` | Yes (`*bold*`, `_italic_`, `~strike~`, `` `code` ``, links, mentions) | Varies by context | Section text, section fields, context elements | - -```typescript -// plain_text — for button labels, header text, input labels -{ type: "plain_text", text: "Submit Request", emoji: true } - -// mrkdwn — for rich content in sections -{ type: "mrkdwn", text: "*Status:* ✅ Deployed to `production`" } -``` - -Rules: -- `header` blocks only accept `plain_text`. -- `section.text` accepts both, but prefer `mrkdwn` for rich formatting. -- `section.fields` accepts both — prefer `mrkdwn` for label-value pairs. -- Button `text` only accepts `plain_text`. -- `context` elements can be `mrkdwn` text objects or `image` elements. -- Set `emoji: true` on `plain_text` objects to render emoji shortcodes like `:rocket:`. - -## Validate Block Limits - -Slack enforces strict limits. Exceeding them causes the API to reject the entire message. - -| Limit | Value | -|-------|-------| -| Blocks per message / modal | 50 | -| Elements per `actions` block | 25 | -| Elements per `context` block | 10 | -| Fields per `section` block | 10 | -| Options per `static_select` / `multi_static_select` | 100 | -| Options per `overflow` menu | 5 | -| Characters in `plain_text` (button) | 75 | -| Characters in `mrkdwn` text (section) | 3000 | -| Characters in modal title | 24 | -| Total message text | 40000 | - -Build a validation utility to check limits before sending: - -```typescript -function validateBlocks(blocks: KnownBlock[]): void { - if (blocks.length > 50) { - throw new Error(`Block limit exceeded: ${blocks.length}/50`); - } - - for (const block of blocks) { - if (block.type === "actions" && block.elements.length > 25) { - throw new Error(`Actions block element limit exceeded: ${block.elements.length}/25`); - } - if (block.type === "context" && block.elements.length > 10) { - throw new Error(`Context block element limit exceeded: ${block.elements.length}/10`); - } - if (block.type === "section" && block.fields && block.fields.length > 10) { - throw new Error(`Section fields limit exceeded: ${block.fields.length}/10`); - } - } -} -``` - -## Use `mrkdwn` Formatting Effectively - -Slack's `mrkdwn` is similar to — but not the same as — Markdown. Key differences: - -| Format | Syntax | Example | -|--------|--------|---------| -| Bold | `*text*` | *bold* | -| Italic | `_text_` | _italic_ | -| Strikethrough | `~text~` | ~struck~ | -| Code | `` `text` `` | `code` | -| Code block | ` ```text``` ` | (preformatted) | -| Link | `<https://url\|label>` | [label](https://url) | -| User mention | `<@U12345678>` | @user | -| Channel mention | `<#C12345678>` | #channel | -| Date/time | `<!date^timestamp^format\|fallback>` | Jan 23, 2025 | -| Emoji | `:emoji_name:` | 🚀 | -| Blockquote | `> text` | (quoted text) | -| Ordered list | `1. item` | 1. item | -| Bulleted list | `• item` or `- item` | • item | - -Notable differences from standard Markdown: -- `**bold**` does **not** work — use `*bold*`. -- `[label](url)` does **not** work — use `<url|label>`. -- Line breaks require `\n`, not double-space. - -## Handle Modal Submissions - -Modal views use `callback_id` to route submissions. Always validate input values before processing and return `response_action: "errors"` for validation failures so the modal stays open. - -```typescript -app.view("feedback_modal", async ({ ack, view, body, client }) => { - const values = view.state.values; - const rating = values.rating_block.rating_input.selected_option?.value; - const comment = values.comment_block.comment_input.value; - - // Validation - const errors: Record<string, string> = {}; - if (!rating) errors.rating_block = "Please select a rating"; - if (comment && comment.length > 500) errors.comment_block = "Comment must be under 500 characters"; - - if (Object.keys(errors).length > 0) { - await ack({ response_action: "errors", errors }); - return; - } - - await ack(); - - // Process submission - await saveFeedback({ userId: body.user.id, rating, comment }); - - // Notify the user via DM - await client.chat.postMessage({ - channel: body.user.id, - text: `Thanks for your feedback! You rated us ${rating}/5.`, - }); -}); -``` - -### Updating and Pushing Modals - -Use `response_action: "update"` to replace the current modal view, or `response_action: "push"` to stack a new view on top (max 3 views deep). - -```typescript -// Update the modal to show a confirmation screen -await ack({ - response_action: "update", - view: { - type: "modal", - title: { type: "plain_text", text: "Confirmed" }, - blocks: [ - { - type: "section", - text: { type: "mrkdwn", text: "✅ Your ticket has been created." }, - }, - ], - }, -}); -``` - -## Additional Guidelines - -- **Fallback text**: Always set the top-level `text` property on messages that use `blocks`. This text appears in notifications, screen readers, and clients that cannot render blocks. -- **Accessibility**: Use descriptive `alt_text` on all `image` elements. Use meaningful button labels — not "Click here". -- **Confirm dialogs**: Add `confirm` objects to destructive buttons to prevent accidental actions. -- **Overflow menus**: Use `overflow` for secondary actions to keep the `actions` block clean. -- **Dynamic options**: For large option sets, use `external_select` with an `options` handler instead of `static_select` to avoid the 100-option limit. -- **Ephemeral messages**: Use `chat.postEphemeral` for messages only the triggering user should see (e.g., validation errors, help text). -- **Message updates**: Use `chat.update` to edit messages in-place (e.g., reflecting the result of a button press) instead of posting new messages. -- **Metadata**: Use `metadata` on messages to attach structured event payloads for downstream automation. -- **Unfurling**: When building link unfurls, return a single `section` with an `image` accessory for compact previews. diff --git a/plugins/slack/rules/slack-bolt.mdc b/plugins/slack/rules/slack-bolt.mdc deleted file mode 100644 index 9d06ded..0000000 --- a/plugins/slack/rules/slack-bolt.mdc +++ /dev/null @@ -1,321 +0,0 @@ ---- -description: Best practices for building Slack apps with the Bolt framework and Slack APIs -globs: - - "**/*.ts" - - "**/*.js" -alwaysApply: false ---- - -# Slack Bolt Framework Best Practices - -## Use the Bolt SDK — Not Raw HTTP Calls - -Always use the official `@slack/bolt` SDK rather than hand-rolling HTTP requests to the Slack API. Bolt handles request verification, event parsing, retry headers, and middleware chaining out of the box. - -```typescript -import { App } from "@slack/bolt"; - -const app = new App({ - token: process.env.SLACK_BOT_TOKEN, - signingSecret: process.env.SLACK_SIGNING_SECRET, -}); -``` - -Never import `axios` or `node-fetch` to call `https://slack.com/api/*` directly. The `@slack/web-api` client (bundled with Bolt) provides typed methods, automatic pagination, and rate-limit handling. - -## Acknowledge Events Immediately with `ack()` - -Every listener — commands, actions, view submissions, shortcuts, options — **must** call `await ack()` within 3 seconds. Perform heavy work after acknowledging. - -```typescript -// Good — ack first, then do work -app.command("/deploy", async ({ ack, say, command }) => { - await ack(); // Respond to Slack within 3 seconds - const result = await runDeployment(command.text); - await say(`Deployment complete: ${result.url}`); -}); - -// Bad — long operation before ack causes timeout -app.command("/deploy", async ({ ack, say, command }) => { - const result = await runDeployment(command.text); // May exceed 3s - await ack(); // Too late — Slack already timed out -}); -``` - -For interactions that need to send an immediate visible response, pass a response body to `ack()`: - -```typescript -app.action("approve_button", async ({ ack, action, say }) => { - await ack({ text: "Processing approval…" }); - await processApproval(action); - await say("Request approved ✓"); -}); -``` - -## Use Block Kit for Rich Messages - -Build messages with Block Kit blocks and elements instead of plain text or legacy attachments. Block Kit provides a consistent, interactive, and accessible UI across all Slack clients. - -```typescript -await say({ - blocks: [ - { - type: "header", - text: { type: "plain_text", text: "Deployment Summary" }, - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "*Environment:*\nProduction" }, - { type: "mrkdwn", text: "*Status:*\n✅ Success" }, - ], - }, - { - type: "actions", - elements: [ - { - type: "button", - text: { type: "plain_text", text: "View Logs" }, - url: "https://example.com/logs", - action_id: "view_logs", - }, - ], - }, - ], - text: "Deployment Summary — fallback for notifications", -}); -``` - -Always include a `text` property alongside `blocks` as a fallback for notifications and accessibility. - -## Validate Request Signatures - -When deploying outside of Socket Mode (i.e., receiving HTTP events), always verify the `x-slack-signature` header to prevent forgery. Bolt does this automatically when you set `signingSecret`, but if you use a custom Express server, make sure the Bolt receiver processes the raw body. - -```typescript -// If using a custom Express receiver, ensure rawBody is available -import { App, ExpressReceiver } from "@slack/bolt"; - -const receiver = new ExpressReceiver({ - signingSecret: process.env.SLACK_SIGNING_SECRET!, -}); - -// Mount additional routes AFTER Bolt middleware so signature verification runs -receiver.router.get("/health", (_req, res) => res.send("ok")); - -const app = new App({ token: process.env.SLACK_BOT_TOKEN!, receiver }); -``` - -Never disable signature verification in production. Never log or expose your signing secret. - -## Handle Rate Limiting with Retry Logic - -Slack API methods return HTTP 429 with a `Retry-After` header when rate-limited. The `@slack/web-api` client retries automatically (up to a configurable limit), but be aware of the default behavior and tune it for your workload. - -```typescript -import { WebClient, LogLevel } from "@slack/web-api"; - -const client = new WebClient(process.env.SLACK_BOT_TOKEN, { - retryConfig: { - retries: 3, - factor: 2, // Exponential backoff - }, - logLevel: LogLevel.WARN, -}); -``` - -For bulk operations (e.g., messaging all members of a channel), add your own delay between calls: - -```typescript -for (const userId of userIds) { - await client.chat.postMessage({ channel: userId, text: message }); - await new Promise((resolve) => setTimeout(resolve, 1200)); // ~50 req/min tier -} -``` - -## Use Socket Mode for Development and Firewalled Environments - -Socket Mode connects to Slack over a WebSocket instead of requiring a public HTTP endpoint. Use it during development, behind corporate firewalls, or when you cannot expose a URL. - -```typescript -const app = new App({ - token: process.env.SLACK_BOT_TOKEN, - appToken: process.env.SLACK_APP_TOKEN, // xapp-1-... token with connections:write scope - socketMode: true, -}); -``` - -Generate an App-Level Token with the `connections:write` scope in the Slack App settings. Do **not** use Socket Mode in high-throughput production deployments — use HTTP mode with a load balancer instead. - -## Request Minimal OAuth Scopes - -Only request the scopes your app actually needs. Excessive scopes erode user trust and increase the blast radius of a token compromise. - -| Scope | Use Case | -|-------|----------| -| `chat:write` | Post messages to channels the bot is a member of | -| `commands` | Register and receive slash commands | -| `app_mentions:read` | Receive `app_mention` events | -| `channels:read` | List public channels | -| `groups:read` | List private channels the bot is in | -| `im:read` / `im:write` | Read and send direct messages | -| `reactions:read` | Track emoji reactions | -| `files:read` | Access shared files | -| `users:read` | Look up user profiles | - -Review scopes when adding new features and remove unused ones. - -## Store Tokens Securely - -Never hard-code `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, or `SLACK_APP_TOKEN` in source code. Use environment variables, a secrets manager (AWS Secrets Manager, Vault, Doppler), or encrypted configuration. - -```typescript -// Good — environment variables -const app = new App({ - token: process.env.SLACK_BOT_TOKEN!, - signingSecret: process.env.SLACK_SIGNING_SECRET!, -}); - -// Bad — hard-coded tokens -const app = new App({ - token: "xoxb-1234567890-abcdefghij", - signingSecret: "abc123def456", -}); -``` - -For multi-workspace apps using OAuth, store installation data (tokens, team IDs) in an encrypted database and implement the `InstallationStore` interface: - -```typescript -import { InstallationStore } from "@slack/bolt"; - -const installationStore: InstallationStore = { - storeInstallation: async (installation) => { - await db.installations.upsert(installation); - }, - fetchInstallation: async (installQuery) => { - return await db.installations.findOne(installQuery); - }, - deleteInstallation: async (installQuery) => { - await db.installations.delete(installQuery); - }, -}; -``` - -## Distinguish `app_mention` vs `message` Events - -- **`app_mention`** — Fires only when the bot is explicitly `@mentioned`. Use it for commands directed at your bot. -- **`message`** — Fires for every message in channels the bot is a member of. Use it for keyword-based triggers or monitoring. - -```typescript -// Respond only when someone @mentions the bot -app.event("app_mention", async ({ event, say }) => { - await say(`Hey <@${event.user}>, how can I help?`); -}); - -// React to any message containing "help" — careful with volume -app.message(/help/i, async ({ message, say }) => { - await say("Here are some things I can do…"); -}); -``` - -Prefer `app_mention` for interactive bot commands to avoid processing every message in busy channels. If you must listen to `message` events, filter aggressively and debounce if needed. - -## Use Modals for Complex Interactions - -For multi-step workflows, forms, or data entry, use Slack modals (views) instead of threaded messages or ephemeral prompts. Modals provide a structured form with input validation. - -```typescript -app.command("/create-ticket", async ({ ack, body, client }) => { - await ack(); - - await client.views.open({ - trigger_id: body.trigger_id, - view: { - type: "modal", - callback_id: "ticket_modal", - title: { type: "plain_text", text: "Create Ticket" }, - submit: { type: "plain_text", text: "Submit" }, - blocks: [ - { - type: "input", - block_id: "title_block", - label: { type: "plain_text", text: "Title" }, - element: { - type: "plain_text_input", - action_id: "title_input", - placeholder: { type: "plain_text", text: "Brief description" }, - }, - }, - { - type: "input", - block_id: "priority_block", - label: { type: "plain_text", text: "Priority" }, - element: { - type: "static_select", - action_id: "priority_select", - options: [ - { text: { type: "plain_text", text: "Low" }, value: "low" }, - { text: { type: "plain_text", text: "Medium" }, value: "medium" }, - { text: { type: "plain_text", text: "High" }, value: "high" }, - ], - }, - }, - ], - }, - }); -}); - -// Handle modal submission -app.view("ticket_modal", async ({ ack, view, body }) => { - const title = view.state.values.title_block.title_input.value; - const priority = view.state.values.priority_block.priority_select.selected_option?.value; - - // Validate - if (!title || title.length < 5) { - await ack({ - response_action: "errors", - errors: { title_block: "Title must be at least 5 characters" }, - }); - return; - } - - await ack(); - await createTicket({ title, priority, userId: body.user.id }); -}); -``` - -## Paginate API Responses - -Many Slack API methods return paginated results. Always handle pagination to avoid missing data. The `@slack/web-api` client provides cursor-based pagination helpers. - -```typescript -// Manual cursor pagination -let cursor: string | undefined; -const allChannels = []; - -do { - const result = await client.conversations.list({ - limit: 200, - cursor, - }); - allChannels.push(...(result.channels ?? [])); - cursor = result.response_metadata?.next_cursor || undefined; -} while (cursor); - -// Or use the built-in pagination helper -for await (const page of client.paginate("conversations.list", { limit: 200 })) { - allChannels.push(...(page.channels ?? [])); -} -``` - -Never assume the first page contains all results. Set `limit` to reduce the number of pages when you only need a few records. - -## Additional Guidelines - -- **Error handling**: Wrap every listener body in try/catch. Unhandled errors crash the Bolt process or leave interactions hanging. -- **Logging**: Use Bolt's built-in logger (`app.logger`) instead of `console.log`. Set `logLevel` to control verbosity. -- **Middleware**: Use Bolt's global and listener middleware for cross-cutting concerns like authentication, logging, and feature flags. -- **App Home**: Use `app.event("app_home_opened")` to render a dynamic Home tab with Block Kit. -- **Unfurl links**: Register link domains and use `app.event("link_shared")` to provide rich previews. -- **Testing**: Mock the Slack client in unit tests. Use `@slack/bolt`'s `App` constructor with `authorize` to inject test tokens. -- **Graceful shutdown**: Handle `SIGTERM` and `SIGINT` to call `await app.stop()` so in-flight events complete. diff --git a/plugins/slack/scripts/setup-slack-app.sh b/plugins/slack/scripts/setup-slack-app.sh deleted file mode 100755 index 82dfbd1..0000000 --- a/plugins/slack/scripts/setup-slack-app.sh +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env bash -# -# setup-slack-app.sh -# -# Validates the local Slack app development environment and checks that -# required tokens, dependencies, and configuration are in place. -# -# Usage: -# ./scripts/setup-slack-app.sh [project-dir] -# -# Arguments: -# project-dir Path to the Slack app project (default: current directory) -# -# Exit codes: -# 0 All checks passed -# 1 One or more checks failed -# -# Dependencies: -# - node (>= 18) -# - npm - -set -euo pipefail - -# ─── Configuration ────────────────────────────────────────────────────────────── - -PROJECT_DIR="${1:-.}" -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -ERRORS=0 -WARNINGS=0 - -# ─── Helper Functions ─────────────────────────────────────────────────────────── - -info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; } -success() { printf "${GREEN}[PASS]${NC} %s\n" "$*"; } -warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; WARNINGS=$((WARNINGS + 1)); } -fail() { printf "${RED}[FAIL]${NC} %s\n" "$*"; ERRORS=$((ERRORS + 1)); } - -command_exists() { command -v "$1" &> /dev/null; } - -# ─── Check 1: Node.js ────────────────────────────────────────────────────────── - -info "── Check 1: Node.js ──" - -if command_exists node; then - NODE_VERSION=$(node --version | sed 's/v//') - NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1) - if [[ "$NODE_MAJOR" -ge 18 ]]; then - success "Node.js v${NODE_VERSION} installed (>= 18 required)" - else - fail "Node.js v${NODE_VERSION} is too old. Version 18+ is required." - fi -else - fail "Node.js is not installed. Install from https://nodejs.org/" -fi -echo "" - -# ─── Check 2: npm ────────────────────────────────────────────────────────────── - -info "── Check 2: npm ──" - -if command_exists npm; then - NPM_VERSION=$(npm --version) - success "npm v${NPM_VERSION} installed" -else - fail "npm is not installed. It should come with Node.js." -fi -echo "" - -# ─── Check 3: Project Structure ──────────────────────────────────────────────── - -info "── Check 3: Project Structure ──" - -if [[ -f "$PROJECT_DIR/package.json" ]]; then - success "package.json found" -else - fail "package.json not found in $PROJECT_DIR — run 'npm init -y' first" -fi - -if [[ -f "$PROJECT_DIR/tsconfig.json" ]]; then - success "tsconfig.json found" -else - warn "tsconfig.json not found — TypeScript is recommended for Slack apps" -fi - -if [[ -d "$PROJECT_DIR/src" ]]; then - success "src/ directory found" -else - warn "src/ directory not found — standard project structure uses src/" -fi -echo "" - -# ─── Check 4: Dependencies ───────────────────────────────────────────────────── - -info "── Check 4: Dependencies ──" - -if [[ -f "$PROJECT_DIR/package.json" ]]; then - check_dep() { - local dep=$1 - local label=$2 - if grep -q "\"$dep\"" "$PROJECT_DIR/package.json"; then - success "$label ($dep) is listed in package.json" - else - fail "$label ($dep) is not installed. Run: npm install $dep" - fi - } - - check_dep "@slack/bolt" "Slack Bolt SDK" - - if grep -q "\"@slack/web-api\"" "$PROJECT_DIR/package.json"; then - success "Slack Web API client (@slack/web-api) is listed in package.json" - else - info "@slack/web-api is bundled with @slack/bolt — standalone install is optional" - fi - - if grep -q "\"dotenv\"" "$PROJECT_DIR/package.json"; then - success "dotenv is listed in package.json" - else - warn "dotenv is not installed. Recommended for managing environment variables. Run: npm install dotenv" - fi -else - warn "Skipping dependency checks — no package.json" -fi -echo "" - -# ─── Check 5: Environment Variables ──────────────────────────────────────────── - -info "── Check 5: Environment Variables ──" - -check_env() { - local var_name=$1 - local description=$2 - local required=$3 - - if [[ -n "${!var_name:-}" ]]; then - # Mask the token for display - local value="${!var_name}" - local masked="${value:0:8}…${value: -4}" - success "$var_name is set ($masked)" - elif [[ -f "$PROJECT_DIR/.env" ]] && grep -q "^${var_name}=" "$PROJECT_DIR/.env" 2>/dev/null; then - success "$var_name is defined in .env file" - else - if [[ "$required" == "required" ]]; then - fail "$var_name is not set — $description" - else - warn "$var_name is not set — $description" - fi - fi -} - -check_env "SLACK_BOT_TOKEN" "Bot User OAuth Token (xoxb-...) from OAuth & Permissions" "required" -check_env "SLACK_SIGNING_SECRET" "Signing Secret from App Credentials page" "required" -check_env "SLACK_APP_TOKEN" "App-Level Token (xapp-...) for Socket Mode" "optional" -echo "" - -# ─── Check 6: .env Security ──────────────────────────────────────────────────── - -info "── Check 6: .env Security ──" - -if [[ -f "$PROJECT_DIR/.env" ]]; then - success ".env file exists" - - if [[ -f "$PROJECT_DIR/.gitignore" ]]; then - if grep -q '\.env' "$PROJECT_DIR/.gitignore"; then - success ".env is listed in .gitignore" - else - fail ".env is NOT in .gitignore — tokens may be committed to git!" - fi - else - warn ".gitignore not found — create one and add .env to prevent token leaks" - fi -else - info "No .env file found — using system environment variables or secrets manager" -fi -echo "" - -# ─── Check 7: Token Format Validation ────────────────────────────────────────── - -info "── Check 7: Token Format ──" - -validate_token_format() { - local var_name=$1 - local prefix=$2 - local label=$3 - - local value="${!var_name:-}" - if [[ -z "$value" ]] && [[ -f "$PROJECT_DIR/.env" ]]; then - value=$(grep "^${var_name}=" "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-) - fi - - if [[ -z "$value" ]]; then - return - fi - - if [[ "$value" == "${prefix}"* ]]; then - success "$label token has correct prefix ($prefix…)" - else - fail "$label token has incorrect format — expected prefix '$prefix'" - fi -} - -validate_token_format "SLACK_BOT_TOKEN" "xoxb-" "Bot" -validate_token_format "SLACK_APP_TOKEN" "xapp-" "App-Level" -echo "" - -# ─── Check 8: Slack App Configuration Reminders ──────────────────────────────── - -info "── Check 8: App Dashboard Checklist ──" - -echo " Please verify the following in your Slack App dashboard (https://api.slack.com/apps):" -echo "" -echo " □ OAuth & Permissions → Bot Token Scopes include at minimum:" -echo " - chat:write" -echo " - app_mentions:read (if using @mentions)" -echo " - commands (if using slash commands)" -echo " □ Event Subscriptions → Enabled, with these bot events subscribed:" -echo " - app_mention" -echo " - message.im (for DM bots)" -echo " □ Socket Mode → Enabled (for local development)" -echo " □ Interactivity & Shortcuts → Enabled (for buttons, modals, selects)" -echo " □ App installed to workspace (reinstall after scope changes)" -echo "" - -# ─── Summary ──────────────────────────────────────────────────────────────────── - -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -if [[ $ERRORS -gt 0 ]]; then - printf "${RED}RESULT: %d error(s), %d warning(s)${NC}\n" "$ERRORS" "$WARNINGS" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - exit 1 -elif [[ $WARNINGS -gt 0 ]]; then - printf "${YELLOW}RESULT: 0 errors, %d warning(s)${NC}\n" "$WARNINGS" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - exit 0 -else - printf "${GREEN}RESULT: All checks passed!${NC}\n" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - exit 0 -fi diff --git a/plugins/twilio/.cursor/plugin.json b/plugins/twilio/.cursor/plugin.json index 7fa9302..ffc1e94 100644 --- a/plugins/twilio/.cursor/plugin.json +++ b/plugins/twilio/.cursor/plugin.json @@ -1,18 +1,28 @@ { "name": "twilio", "version": "1.0.0", - "description": "Cursor plugin for Twilio — SMS, Voice, WhatsApp, Verify, and communications APIs", - "author": { "name": "Cursor", "email": "plugins@cursor.com" }, + "description": "Cursor plugin for Twilio \u2014 SMS, Voice, WhatsApp, Verify, and communications APIs", + "author": { + "name": "Cursor", + "email": "plugins@cursor.com" + }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["twilio", "sms", "voice", "whatsapp", "communications", "verify"], + "keywords": [ + "twilio", + "sms", + "voice", + "whatsapp", + "communications", + "verify" + ], "category": "saas", - "tags": ["communications", "sms", "voice"], - "agents": "./agents/", + "tags": [ + "communications", + "sms", + "voice" + ], "skills": "./skills/", - "rules": "./rules/", - "hooks": "./hooks/hooks.json", - "mcpServers": "./mcp.json", - "extensions": "./extensions/" + "mcpServers": "./mcp.json" } diff --git a/plugins/twilio/CHANGELOG.md b/plugins/twilio/CHANGELOG.md index 664d443..5f2859d 100644 --- a/plugins/twilio/CHANGELOG.md +++ b/plugins/twilio/CHANGELOG.md @@ -1,30 +1,13 @@ # Changelog -All notable changes to the Twilio plugin for Cursor will be documented in this file. +All notable changes to this plugin will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-08 +## [Unreleased] -### Added +## [1.0.0] - 2026-02-07 -- **Plugin manifest** (`.cursor/plugin.json`) with full metadata and configuration paths. -- **Rules**: - - `twilio-integration.mdc` — Best practices for Twilio SDK usage, credential management via environment variables, E.164 phone number formatting, Lookup API validation, Messaging Services for scale, error handling with Twilio error codes, rate limiting with exponential backoff, delivery status callbacks, TwiML builders for voice/messaging, and environment variable validation. - - `twilio-webhooks.mdc` — Best practices for webhook request signature validation using `twilio.webhook()` middleware, TwiML responses, 15-second response timeout, status callbacks for message delivery and call progress, idempotent webhook processing, ngrok for local development, and Next.js webhook handlers. -- **Agent**: - - `twilio-communications-agent.md` — AI agent for guiding developers through Twilio product selection (SMS, Voice, WhatsApp, Verify, SendGrid), Messaging Service configuration, A2P 10DLC compliance, IVR design patterns, cost optimization, and multi-channel communications architecture. -- **Skills**: - - `setup-sms/SKILL.md` — Step-by-step guide for implementing SMS messaging with Twilio, including SDK setup, sending/receiving messages, Messaging Services, delivery tracking, error handling with retry logic, and local testing with ngrok. - - `setup-verify/SKILL.md` — Step-by-step guide for implementing phone verification with Twilio Verify, including service creation, send/check API endpoints, frontend verification flow, multi-channel fallback, and error handling. -- **Hooks**: - - Pre-commit hook to detect and block Twilio Auth Tokens and API Key Secrets from being committed. -- **Scripts**: - - `check-twilio-credentials.sh` — Shell script for scanning staged files for Twilio credentials. -- **MCP Server**: - - `mcp.json` — Configuration for the Twilio MCP server for direct API interaction from Cursor. -- **Documentation**: - - `README.md` — Plugin overview, structure, setup instructions, and resources. - - `CHANGELOG.md` — This changelog. - - `LICENSE` — MIT license. +### Added +- Initial plugin with skills and MCP server configuration diff --git a/plugins/twilio/README.md b/plugins/twilio/README.md index d6595b3..640b443 100644 --- a/plugins/twilio/README.md +++ b/plugins/twilio/README.md @@ -1,113 +1,28 @@ -# Twilio Plugin for Cursor +# Twilio Plugin -A comprehensive Cursor plugin for integrating Twilio's communications APIs. Provides rules, agents, skills, hooks, and MCP server configuration to help developers build SMS messaging, voice applications, WhatsApp integrations, phone verification, and more with Twilio. +Cursor plugin for Twilio — SMS, Voice, WhatsApp, Verify, and communications APIs. -## Features +## Installation -- **Rules** — Enforced best practices for Twilio SDK usage and webhook handling -- **Agent** — AI-powered assistant for designing messaging flows, voice apps, and verification systems -- **Skills** — Step-by-step guides for setting up SMS messaging and phone verification -- **Hooks** — Pre-commit hook to prevent committing Twilio credentials -- **MCP Server** — Direct Twilio API interaction from within Cursor - -## Plugin Structure - -``` -plugins/twilio/ -├── .cursor/ -│ └── plugin.json # Plugin manifest -├── agents/ -│ └── twilio-communications-agent.md # Communications agent -├── extensions/ # Reserved for future extensions -├── hooks/ -│ └── hooks.json # Hook definitions -├── rules/ -│ ├── twilio-integration.mdc # Integration best practices -│ └── twilio-webhooks.mdc # Webhook best practices -├── scripts/ -│ └── check-twilio-credentials.sh # Credential detection script -├── skills/ -│ ├── setup-sms/ -│ │ └── SKILL.md # SMS messaging setup guide -│ └── setup-verify/ -│ └── SKILL.md # Phone verification setup guide -├── mcp.json # MCP server configuration -├── CHANGELOG.md -├── LICENSE -└── README.md -``` - -## Getting Started - -### Prerequisites - -- A [Twilio account](https://www.twilio.com/try-twilio) (free trial works for testing) -- Node.js 18+ -- A Twilio phone number with SMS/Voice capability -- [ngrok](https://ngrok.com/) for local webhook testing - -### Environment Variables - -Add the following to your project's `.env` file: - -```env -TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -TWILIO_AUTH_TOKEN=your_auth_token_here -TWILIO_PHONE_NUMBER=+15017122661 -TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -TWILIO_VERIFY_SERVICE_SID=VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +```bash +agent install twilio ``` -Find your credentials at the [Twilio Console](https://console.twilio.com). - -### Using the MCP Server - -The plugin includes configuration for the Twilio MCP server, which allows Cursor to interact with Twilio APIs directly. Make sure your `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` environment variables are set. - -### Rules - -The plugin enforces best practices automatically: - -- **twilio-integration** — Applied to all `.ts` and `.js` files. Covers credential management, E.164 phone formatting, error handling, rate limiting, TwiML builders, Messaging Services, and delivery callbacks. -- **twilio-webhooks** — Applied to webhook and Twilio-related files. Covers signature validation, TwiML responses, status callbacks, idempotent processing, and 15-second timeout compliance. - -### Agent - -Ask the Twilio Communications Agent for help with: - -- Setting up SMS, MMS, and WhatsApp messaging -- Building IVR systems and voice applications -- Implementing phone verification with Twilio Verify -- Choosing between Twilio products (Messaging vs. Verify vs. Conversations) -- A2P 10DLC compliance for US messaging -- Optimizing messaging costs +## Components ### Skills -Follow the step-by-step skills to set up: +| Skill | Description | +|:------|:------------| +| `setup-sms` | Send and receive SMS/MMS with webhook handlers and delivery tracking | +| `setup-verify` | Phone verification with Twilio Verify, multi-channel fallback, and error handling | -1. **SMS Messaging** — Send and receive SMS/MMS messages with Twilio Programmable Messaging, including Messaging Services, webhooks, and delivery tracking -2. **Phone Verification** — Implement OTP-based phone verification with Twilio Verify, including multi-channel fallback and fraud protection - -### Pre-Commit Hook - -The pre-commit hook scans staged files for Twilio Auth Tokens and API Key Secrets and blocks the commit if any are found. Account SIDs are allowed since they are not secret. - -To set up the hook: - -```bash -chmod +x plugins/twilio/scripts/check-twilio-credentials.sh -``` +### MCP Server -## Resources +Provides Twilio API access via `@anthropic/twilio-mcp-server`. -- [Twilio Documentation](https://www.twilio.com/docs) -- [Twilio API Reference](https://www.twilio.com/docs/api) -- [Twilio Console](https://console.twilio.com) -- [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart) -- [Twilio Helper Libraries](https://www.twilio.com/docs/libraries) -- [Twilio Code Exchange (Sample Apps)](https://www.twilio.com/code-exchange) +Requires `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` environment variables. ## License -MIT License — see [LICENSE](./LICENSE) for details. +MIT diff --git a/plugins/twilio/agents/twilio-communications-agent.md b/plugins/twilio/agents/twilio-communications-agent.md deleted file mode 100644 index 9ebf283..0000000 --- a/plugins/twilio/agents/twilio-communications-agent.md +++ /dev/null @@ -1,223 +0,0 @@ -# Twilio Communications Agent - -## Identity - -You are a Twilio communications specialist. You help developers build messaging, voice, and verification systems using Twilio's APIs and best practices. You have deep knowledge of Twilio's product suite — including Programmable Messaging, Programmable Voice, Twilio Verify, WhatsApp Business API, SendGrid Email, and Twilio Conversations — and can guide developers through complex communications workflows. - -## Expertise Areas - -- **SMS & MMS**: Sending and receiving text messages, Messaging Services, number pooling, opt-out handling, A2P 10DLC compliance, short codes, and toll-free numbers -- **Voice**: Inbound and outbound calls, IVR menus, call recording, conferencing, call forwarding, voicemail, speech recognition, and TwiML -- **WhatsApp**: WhatsApp Business API integration, template messages, session messages, media messages, and interactive messages -- **Verify**: Phone number verification, OTP delivery via SMS/voice/email/WhatsApp, TOTP support, and silent network authentication -- **Email (SendGrid)**: Transactional email, marketing campaigns, templates, email validation, and sender authentication -- **Conversations**: Multi-channel conversations, chat, participant management, and message history -- **Lookup**: Phone number validation, carrier lookup, line type intelligence, and caller name lookup - -## Guidelines - -### Product Selection - -When a user wants to add communications capabilities, help them choose the right Twilio products: - -1. **Transactional SMS (Notifications, Alerts, OTP)** - - Use Programmable Messaging with a Messaging Service - - Use Twilio Verify for OTP/verification codes (not raw SMS) - - Best for: order confirmations, appointment reminders, 2FA codes, shipping updates - - Consider: A2P 10DLC registration required for US traffic - -2. **Two-Factor Authentication / Phone Verification** - - Use Twilio Verify API (not raw SMS) - - Built-in rate limiting, fraud detection, and channel fallback - - Supports SMS, voice call, email, WhatsApp, and TOTP - - Best for: user registration, login verification, account recovery - -3. **Marketing / Promotional Messaging** - - Use Messaging Services with proper opt-in/opt-out handling - - Must comply with TCPA, CTIA, and carrier guidelines - - Register for A2P 10DLC (US) or use short codes - - Best for: promotions, campaigns, newsletters via SMS - -4. **WhatsApp Messaging** - - Use the WhatsApp Business API via Twilio - - Template messages for outbound (must be pre-approved) - - Session messages for replies within 24-hour window - - Best for: customer support, order updates, appointment booking - -5. **Voice Applications (IVR, Call Centers)** - - Use Programmable Voice with TwiML - - Twilio Flex for full contact center solution - - Best for: customer support lines, appointment scheduling, automated surveys - -6. **Transactional Email** - - Use Twilio SendGrid - - Dynamic templates for personalized emails - - Best for: welcome emails, receipts, password resets, notifications - -### Messaging Service vs. Direct Number - -Guide users on when to use each: - -- **Messaging Service (Recommended for production)** - - Number pooling for higher throughput - - Sticky sender (same number per recipient) - - Automatic compliance features - - Intelligent number selection - - Link shortening and tracking - - Use for: any production SMS workload - -- **Direct number (from parameter)** - - Simpler setup for prototyping - - Single number, lower throughput - - Use for: development, testing, very low-volume sends - -### Cost Optimization - -Help users minimize their Twilio costs: - -- **Use Twilio Verify instead of raw SMS for OTP** — Verify has built-in fraud protection and only charges for successful verifications -- **Use Messaging Services** — intelligent routing selects the cheapest available number -- **Validate numbers with Lookup API** before sending to avoid charges for invalid numbers -- **Implement opt-out handling** — stop sending to opted-out numbers (saves money and avoids carrier fines) -- **Use WhatsApp for international messages** — often cheaper than international SMS -- **Monitor usage** — set up billing alerts and usage triggers in the Twilio Console -- **Use local numbers** where possible — local SMS is cheaper than short code or toll-free -- **Batch operations** — use bulk messaging features instead of individual API calls - -### A2P 10DLC Compliance (US) - -Guide users through US messaging compliance: - -1. **Register your Brand** in the Twilio Console -2. **Create a Campaign** (use case registration) -3. **Associate numbers** with the Campaign -4. **Use Messaging Services** for sending -5. **Handle opt-out keywords** (STOP, CANCEL, UNSUBSCRIBE, etc.) - -```typescript -// Twilio automatically handles STOP/START keywords when using Messaging Services -// For custom opt-out handling: -app.post('/api/webhooks/twilio/sms', (req, res) => { - const { Body, From, OptOutType } = req.body; - const response = new twilio.twiml.MessagingResponse(); - - if (OptOutType === 'STOP') { - // User opted out — record in your database - await db.subscriber.update({ - where: { phone: From }, - data: { optedOut: true, optedOutAt: new Date() }, - }); - // Don't send a reply — Twilio handles the STOP confirmation - } - - res.type('text/xml').send(response.toString()); -}); -``` - -### IVR Design Patterns - -Help users build effective voice applications: - -```typescript -import twilio from 'twilio'; - -// Main IVR menu -app.post('/api/twilio/voice/welcome', (req, res) => { - const response = new twilio.twiml.VoiceResponse(); - - response.say({ voice: 'Polly.Joanna' }, 'Thank you for calling Acme Inc.'); - - const gather = response.gather({ - input: ['dtmf', 'speech'], - timeout: 5, - numDigits: 1, - action: '/api/twilio/voice/route', - speechTimeout: 'auto', - hints: 'sales, support, billing, hours', - }); - gather.say('Press 1 or say sales for the sales team.'); - gather.say('Press 2 or say support for technical support.'); - gather.say('Press 3 or say billing for billing inquiries.'); - - // No input fallback - response.redirect('/api/twilio/voice/welcome'); - - res.type('text/xml').send(response.toString()); -}); - -// Route based on input -app.post('/api/twilio/voice/route', (req, res) => { - const response = new twilio.twiml.VoiceResponse(); - const digit = req.body.Digits; - const speech = req.body.SpeechResult?.toLowerCase(); - - if (digit === '1' || speech?.includes('sales')) { - response.say('Connecting you to our sales team.'); - response.dial({ callerId: process.env.TWILIO_PHONE_NUMBER }).queue('sales'); - } else if (digit === '2' || speech?.includes('support')) { - response.say('Connecting you to technical support.'); - response.dial({ callerId: process.env.TWILIO_PHONE_NUMBER }).queue('support'); - } else if (digit === '3' || speech?.includes('billing')) { - response.say('Connecting you to billing.'); - response.dial({ callerId: process.env.TWILIO_PHONE_NUMBER }).queue('billing'); - } else { - response.say('Sorry, I didn\'t understand that.'); - response.redirect('/api/twilio/voice/welcome'); - } - - res.type('text/xml').send(response.toString()); -}); -``` - -### Twilio Verify Integration - -Guide proper verification implementation: - -```typescript -// Send a verification code -async function sendVerificationCode(to: string, channel: 'sms' | 'call' | 'email' | 'whatsapp' = 'sms') { - const verification = await client.verify.v2 - .services(process.env.TWILIO_VERIFY_SERVICE_SID!) - .verifications.create({ - to, - channel, - }); - - return verification.status; // 'pending' -} - -// Check a verification code -async function checkVerificationCode(to: string, code: string) { - try { - const check = await client.verify.v2 - .services(process.env.TWILIO_VERIFY_SERVICE_SID!) - .verificationChecks.create({ - to, - code, - }); - - return check.status; // 'approved' or 'pending' - } catch (err: any) { - if (err.code === 20404) { - // Verification not found or expired - return 'expired'; - } - throw err; - } -} -``` - -## Behavioral Rules - -1. **Always recommend Twilio Verify over raw SMS for OTP/verification** — it includes fraud protection, rate limiting, and multi-channel support. -2. **Always recommend Messaging Services for production SMS** — not direct phone numbers. -3. **Always include webhook signature validation** in webhook handler code. -4. **Always use environment variables** for credentials — never hardcode Account SID or Auth Token. -5. **Always use E.164 phone number format** in all code examples. -6. **Always mention A2P 10DLC registration** when discussing US SMS sending. -7. **Always include error handling** with specific Twilio error codes in code examples. -8. **Always use TwiML builders** — never construct TwiML as raw strings. -9. **Proactively mention compliance requirements**: opt-out handling, consent, TCPA for US, GDPR for EU. -10. **Recommend the Twilio CLI and ngrok** for local development and testing. -11. **Stay current** with Twilio's latest API versions and product updates. -12. **Consider cost implications** when suggesting solutions — help users choose the most cost-effective approach. diff --git a/plugins/twilio/hooks/hooks.json b/plugins/twilio/hooks/hooks.json deleted file mode 100644 index f6fe2ae..0000000 --- a/plugins/twilio/hooks/hooks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://cursor.com/schemas/hooks.json", - "hooks": [ - { - "event": "pre-commit", - "script": "./scripts/check-twilio-credentials.sh", - "description": "Prevents committing live Twilio credentials (Auth Tokens, API Key Secrets) to the repository. Account SIDs and test credentials are allowed.", - "enabled": true - } - ] -} diff --git a/plugins/twilio/rules/twilio-integration.mdc b/plugins/twilio/rules/twilio-integration.mdc deleted file mode 100644 index 9b144f0..0000000 --- a/plugins/twilio/rules/twilio-integration.mdc +++ /dev/null @@ -1,312 +0,0 @@ ---- -description: Best practices for Twilio communications integration — SDK usage, credential management, SMS/Voice/WhatsApp patterns, and error handling -globs: - - "**/*.ts" - - "**/*.js" -alwaysApply: false ---- - -# Twilio Integration Best Practices - -## Credential Management - -- **Always use environment variables for Twilio credentials.** Never hardcode Account SID, Auth Token, or API keys in source code. - -```typescript -// ✅ Correct — use environment variables -import twilio from 'twilio'; - -const client = twilio( - process.env.TWILIO_ACCOUNT_SID!, - process.env.TWILIO_AUTH_TOKEN! -); - -// ❌ Incorrect — never hardcode credentials -const client = twilio('AC1234567890abcdef', '0123456789abcdef'); -``` - -- Use consistent environment variable names for Twilio configuration: - - `TWILIO_ACCOUNT_SID` — Account SID (starts with `AC`) - - `TWILIO_AUTH_TOKEN` — Auth Token - - `TWILIO_API_KEY_SID` — API Key SID (for API key auth) - - `TWILIO_API_KEY_SECRET` — API Key Secret - - `TWILIO_PHONE_NUMBER` — Default Twilio phone number (E.164 format) - - `TWILIO_MESSAGING_SERVICE_SID` — Messaging Service SID (starts with `MG`) - - `TWILIO_VERIFY_SERVICE_SID` — Verify Service SID (starts with `VA`) - -## Phone Number Formatting - -- **Always use E.164 format for phone numbers.** Twilio requires phone numbers in E.164 format (`+` followed by country code and number, no spaces or dashes). - -```typescript -// ✅ Correct — E.164 format -const to = '+14155551234'; -const from = '+15017122661'; - -// ❌ Incorrect — never use local or formatted numbers -const to = '(415) 555-1234'; -const to = '415-555-1234'; -const to = '4155551234'; -``` - -- **Validate phone numbers with the Lookup API** before sending messages to avoid errors and wasted costs. - -```typescript -async function validatePhoneNumber(phoneNumber: string): Promise<boolean> { - try { - const lookup = await client.lookups.v2 - .phoneNumbers(phoneNumber) - .fetch({ fields: 'line_type_intelligence' }); - - console.log(`Number: ${lookup.phoneNumber}, Valid: ${lookup.valid}`); - return lookup.valid; - } catch (err) { - console.error(`Lookup failed for ${phoneNumber}:`, err); - return false; - } -} -``` - -## Sending SMS Messages - -- **Use Messaging Services for scale** instead of sending from a single phone number. Messaging Services handle number pooling, sticky sender, and compliance automatically. - -```typescript -// ✅ Recommended — use a Messaging Service for production -const message = await client.messages.create({ - body: 'Your order has shipped! Tracking: 1Z999AA10123456784', - messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!, - to: '+14155551234', -}); - -// Acceptable for simple use cases — use a specific From number -const message = await client.messages.create({ - body: 'Your verification code is 123456', - from: process.env.TWILIO_PHONE_NUMBER!, - to: '+14155551234', -}); -``` - -- **Always include a status callback URL** to track message delivery in production. - -```typescript -const message = await client.messages.create({ - body: 'Your appointment is confirmed for 2pm tomorrow.', - messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!, - to: '+14155551234', - statusCallback: 'https://yourapp.com/api/webhooks/twilio/message-status', -}); - -console.log(`Message SID: ${message.sid}, Status: ${message.status}`); -``` - -## WhatsApp Messaging - -- **Use the WhatsApp-prefixed number format** for WhatsApp messages. Prefix both `to` and `from` numbers with `whatsapp:`. - -```typescript -// Send a WhatsApp message -const message = await client.messages.create({ - body: 'Your order #1234 has been confirmed!', - from: 'whatsapp:+14155238886', // Your Twilio WhatsApp-enabled number - to: 'whatsapp:+14155551234', -}); - -// Use pre-approved templates for outbound WhatsApp messages -// Free-form messages are only allowed within the 24-hour session window -``` - -## Voice Calls and TwiML - -- **Use TwiML builders** for constructing voice and messaging responses. Never build TwiML XML strings manually. - -```typescript -import { twiml } from 'twilio'; - -// ✅ Correct — use the VoiceResponse builder -const response = new twiml.VoiceResponse(); -response.say({ voice: 'alice', language: 'en-US' }, 'Hello! How can we help you today?'); -response.gather({ - input: ['dtmf', 'speech'], - timeout: 5, - numDigits: 1, - action: '/api/twilio/handle-input', -}).say('Press 1 for sales, 2 for support, or say your request.'); -response.say('We didn\'t receive any input. Goodbye!'); -response.hangup(); - -// Returns valid TwiML XML -console.log(response.toString()); - -// ❌ Incorrect — never build TwiML as raw strings -const xml = '<Response><Say>Hello</Say></Response>'; // fragile and error-prone -``` - -```typescript -// MessagingResponse for SMS/WhatsApp webhooks -const response = new twiml.MessagingResponse(); -response.message('Thanks for contacting us! We\'ll get back to you shortly.'); -``` - -## Error Handling - -- **Handle Twilio errors properly** by checking error codes. Twilio errors include specific codes that indicate the failure reason. - -```typescript -try { - const message = await client.messages.create({ - body: 'Hello!', - from: process.env.TWILIO_PHONE_NUMBER!, - to: '+14155551234', - }); - console.log(`Message sent: ${message.sid}`); -} catch (err: any) { - // Twilio REST API errors have a `code` and `status` property - console.error(`Twilio error ${err.code}: ${err.message}`); - - switch (err.code) { - case 21211: // Invalid 'To' phone number - console.error('The destination phone number is invalid.'); - break; - case 21608: // Unverified phone number (trial accounts) - console.error('This number is not verified. Verify it in the Twilio console.'); - break; - case 21610: // Message blocked (recipient opted out) - console.error('The recipient has opted out of messages.'); - break; - case 21614: // 'To' number not a valid mobile number - console.error('The number is not a valid mobile number for SMS.'); - break; - case 30004: // Message blocked - console.error('Message was blocked by the carrier.'); - break; - case 30005: // Unknown destination handset - console.error('The destination handset is unknown or unreachable.'); - break; - case 30006: // Landline or unreachable carrier - console.error('Cannot send SMS to this number (landline or unreachable).'); - break; - case 30008: // Unknown error - console.error('An unknown delivery error occurred.'); - break; - case 20429: // Too many requests (rate limit) - console.error('Rate limit exceeded. Implement backoff and retry.'); - break; - default: - console.error(`Unhandled Twilio error code: ${err.code}`); - } -} -``` - -## Rate Limiting and Retry Logic - -- **Respect Twilio's API rate limits.** Twilio enforces rate limits on API calls — typically 100 requests per second for messaging. Use exponential backoff for retries. - -```typescript -async function sendMessageWithRetry( - to: string, - body: string, - maxRetries = 3 -): Promise<string> { - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - const message = await client.messages.create({ - body, - messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!, - to, - }); - return message.sid; - } catch (err: any) { - // Only retry on rate limits (429) or server errors (5xx) - if (err.code === 20429 || (err.status >= 500 && err.status < 600)) { - if (attempt < maxRetries) { - const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s - console.warn(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`); - await new Promise((resolve) => setTimeout(resolve, delay)); - continue; - } - } - throw err; // Non-retryable error or max retries exceeded - } - } - throw new Error('Max retries exceeded'); -} -``` - -## Delivery Status Callbacks - -- **Handle message delivery status callbacks** to track the full lifecycle of messages (queued → sent → delivered → failed). - -```typescript -// Express handler for message status callbacks -app.post('/api/webhooks/twilio/message-status', express.urlencoded({ extended: false }), (req, res) => { - const { MessageSid, MessageStatus, To, ErrorCode, ErrorMessage } = req.body; - - console.log(JSON.stringify({ - level: 'info', - source: 'twilio_status_callback', - messageSid: MessageSid, - status: MessageStatus, - to: To, - errorCode: ErrorCode || null, - errorMessage: ErrorMessage || null, - })); - - // Update your database with the delivery status - // Statuses: queued, sent, delivered, undelivered, failed - if (MessageStatus === 'failed' || MessageStatus === 'undelivered') { - console.error(`Message ${MessageSid} failed: ${ErrorCode} — ${ErrorMessage}`); - } - - res.sendStatus(200); -}); -``` - -## Making Voice Calls - -- **Use proper TwiML or TwiML Bin URLs** when initiating outbound calls. - -```typescript -// Initiate an outbound voice call -const call = await client.calls.create({ - to: '+14155551234', - from: process.env.TWILIO_PHONE_NUMBER!, - url: 'https://yourapp.com/api/twilio/voice/outbound', // TwiML endpoint - statusCallback: 'https://yourapp.com/api/twilio/voice/status', - statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'], - statusCallbackMethod: 'POST', - machineDetection: 'DetectMessageEnd', // Optional: detect answering machines -}); - -console.log(`Call SID: ${call.sid}`); -``` - -## Environment Variable Validation - -- **Validate required environment variables at startup** to fail fast if credentials are missing. - -```typescript -function validateTwilioConfig() { - const required = [ - 'TWILIO_ACCOUNT_SID', - 'TWILIO_AUTH_TOKEN', - ]; - - const missing = required.filter((key) => !process.env[key]); - - if (missing.length > 0) { - throw new Error( - `Missing required Twilio environment variables: ${missing.join(', ')}. ` + - 'See https://www.twilio.com/docs/usage/api for setup instructions.' - ); - } - - // Validate SID format - if (!process.env.TWILIO_ACCOUNT_SID!.startsWith('AC')) { - throw new Error( - 'TWILIO_ACCOUNT_SID must start with "AC". ' + - 'Find your Account SID at https://console.twilio.com' - ); - } -} -``` diff --git a/plugins/twilio/rules/twilio-webhooks.mdc b/plugins/twilio/rules/twilio-webhooks.mdc deleted file mode 100644 index 2de3ad1..0000000 --- a/plugins/twilio/rules/twilio-webhooks.mdc +++ /dev/null @@ -1,354 +0,0 @@ ---- -description: Best practices for handling Twilio webhooks — request signature validation, TwiML responses, status callbacks, and idempotent processing -globs: - - "**/webhook*.*" - - "**/twilio*.*" -alwaysApply: false ---- - -# Twilio Webhook Handling Best Practices - -## Always Validate Request Signatures - -- **Every Twilio webhook endpoint must validate the request signature** to ensure the request originated from Twilio. Use the `twilio.webhook()` Express middleware or manual validation with `Twilio.validateRequest()`. - -```typescript -import twilio from 'twilio'; -import express from 'express'; - -const app = express(); - -// ✅ Option 1 (Recommended) — Use the twilio.webhook() middleware -// This automatically validates the X-Twilio-Signature header -app.post( - '/api/webhooks/twilio/sms', - express.urlencoded({ extended: false }), - twilio.webhook({ authToken: process.env.TWILIO_AUTH_TOKEN }), - (req, res) => { - // Request is authenticated — process the incoming message - const { From, Body } = req.body; - console.log(`Message from ${From}: ${Body}`); - - const twimlResponse = new twilio.twiml.MessagingResponse(); - twimlResponse.message('Thanks for your message!'); - res.type('text/xml').send(twimlResponse.toString()); - } -); -``` - -```typescript -// ✅ Option 2 — Manual signature validation -import { validateRequest } from 'twilio'; - -function validateTwilioSignature(req: express.Request): boolean { - const signature = req.headers['x-twilio-signature'] as string; - const url = `${process.env.APP_URL}${req.originalUrl}`; - const params = req.body; - - return validateRequest( - process.env.TWILIO_AUTH_TOKEN!, - signature, - url, - params - ); -} - -app.post('/api/webhooks/twilio/sms', express.urlencoded({ extended: false }), (req, res) => { - if (!validateTwilioSignature(req)) { - console.error('Invalid Twilio signature — rejecting request'); - return res.status(403).send('Forbidden'); - } - - // Process the authenticated request - const response = new twilio.twiml.MessagingResponse(); - response.message('Message received!'); - res.type('text/xml').send(response.toString()); -}); -``` - -```typescript -// ❌ Incorrect — never process Twilio webhooks without signature validation -app.post('/api/webhooks/twilio/sms', (req, res) => { - // UNSAFE: anyone can POST to this endpoint and trigger actions - processMessage(req.body.From, req.body.Body); -}); -``` - -## Always Respond with TwiML - -- **Twilio webhook endpoints must respond with valid TwiML** (or an empty `<Response/>` if no action is needed). Always use the TwiML builder classes instead of constructing XML strings. - -```typescript -import twilio from 'twilio'; - -// SMS/WhatsApp webhook — respond with MessagingResponse -app.post('/api/webhooks/twilio/sms', (req, res) => { - const response = new twilio.twiml.MessagingResponse(); - const incomingBody = req.body.Body?.toLowerCase() || ''; - - if (incomingBody.includes('help')) { - response.message('Available commands: HELP, STATUS, STOP'); - } else if (incomingBody.includes('status')) { - response.message('All systems operational.'); - } else { - response.message('Reply HELP for available commands.'); - } - - res.type('text/xml').send(response.toString()); -}); - -// Voice webhook — respond with VoiceResponse -app.post('/api/webhooks/twilio/voice', (req, res) => { - const response = new twilio.twiml.VoiceResponse(); - - response.say({ voice: 'Polly.Joanna' }, 'Welcome to Acme Support.'); - const gather = response.gather({ - numDigits: 1, - action: '/api/webhooks/twilio/voice/menu', - method: 'POST', - }); - gather.say('Press 1 for sales. Press 2 for support.'); - - // If no input, repeat - response.redirect('/api/webhooks/twilio/voice'); - - res.type('text/xml').send(response.toString()); -}); - -// Empty response when no reply is needed -app.post('/api/webhooks/twilio/status', (req, res) => { - const response = new twilio.twiml.MessagingResponse(); - // No messages added — returns empty <Response/> - res.type('text/xml').send(response.toString()); -}); -``` - -## Respond Within 15 Seconds - -- **Twilio expects a response within 15 seconds** for webhooks. If your endpoint takes longer, Twilio will time out and may retry or return an error to the caller. Respond immediately and process asynchronously for any heavy work. - -```typescript -// ✅ Correct — respond immediately, process async -app.post('/api/webhooks/twilio/sms', express.urlencoded({ extended: false }), (req, res) => { - const { From, Body, MessageSid } = req.body; - - // Send TwiML response immediately - const response = new twilio.twiml.MessagingResponse(); - response.message('Got it! We\'re processing your request.'); - res.type('text/xml').send(response.toString()); - - // Process asynchronously (use a job queue in production) - processIncomingMessage({ from: From, body: Body, sid: MessageSid }) - .catch((err) => console.error(`Error processing message ${MessageSid}:`, err)); -}); - -// ❌ Incorrect — long-running processing before responding -app.post('/api/webhooks/twilio/sms', async (req, res) => { - await doSlowDatabaseQuery(req.body); // This might take > 15s - await callExternalAPI(req.body); // Adding more latency - const response = new twilio.twiml.MessagingResponse(); - response.message('Done!'); - res.type('text/xml').send(response.toString()); // Twilio may have already timed out -}); -``` - -## Handle Status Callbacks - -- **Implement status callback endpoints** for tracking message delivery and call progress. Twilio sends status updates as POST requests to your callback URLs. - -```typescript -// Message delivery status callback -app.post( - '/api/webhooks/twilio/message-status', - express.urlencoded({ extended: false }), - twilio.webhook({ authToken: process.env.TWILIO_AUTH_TOKEN }), - async (req, res) => { - const { - MessageSid, - MessageStatus, - To, - From, - ErrorCode, - ErrorMessage, - } = req.body; - - // Log the status update - console.log(JSON.stringify({ - level: MessageStatus === 'failed' ? 'error' : 'info', - source: 'twilio_message_status', - messageSid: MessageSid, - status: MessageStatus, // queued, sent, delivered, undelivered, failed - to: To, - from: From, - errorCode: ErrorCode || undefined, - errorMessage: ErrorMessage || undefined, - })); - - // Update delivery status in your database - await db.message.update({ - where: { twilioSid: MessageSid }, - data: { - deliveryStatus: MessageStatus, - errorCode: ErrorCode || null, - updatedAt: new Date(), - }, - }); - - res.sendStatus(200); - } -); - -// Call status callback -app.post( - '/api/webhooks/twilio/call-status', - express.urlencoded({ extended: false }), - twilio.webhook({ authToken: process.env.TWILIO_AUTH_TOKEN }), - async (req, res) => { - const { - CallSid, - CallStatus, - CallDuration, - To, - From, - } = req.body; - - console.log(JSON.stringify({ - level: 'info', - source: 'twilio_call_status', - callSid: CallSid, - status: CallStatus, // queued, ringing, in-progress, completed, busy, failed, no-answer - duration: CallDuration, - to: To, - from: From, - })); - - await db.call.update({ - where: { twilioSid: CallSid }, - data: { - status: CallStatus, - duration: CallDuration ? parseInt(CallDuration, 10) : null, - updatedAt: new Date(), - }, - }); - - res.sendStatus(200); - } -); -``` - -## Implement Idempotent Processing - -- **Twilio may send the same webhook multiple times** (e.g., on retries after timeouts). Always deduplicate webhook requests using the message or call SID. - -```typescript -async function processIncomingMessage(params: { - sid: string; - from: string; - body: string; -}) { - // Check if this message was already processed - const existing = await db.incomingMessage.findUnique({ - where: { twilioSid: params.sid }, - }); - - if (existing) { - console.log(`Message ${params.sid} already processed, skipping`); - return; - } - - // Process the message - await handleMessage(params.from, params.body); - - // Record that we processed this message - await db.incomingMessage.create({ - data: { - twilioSid: params.sid, - from: params.from, - body: params.body, - processedAt: new Date(), - }, - }); -} -``` - -## URL Configuration for Different Environments - -- **Use full, publicly accessible URLs** for webhook endpoints. Twilio cannot reach `localhost`. For local development, use a tunneling tool like ngrok. - -```bash -# Local development — use ngrok to expose your local server -ngrok http 3000 - -# Set the ngrok URL as your webhook in the Twilio console -# https://abc123.ngrok.io/api/webhooks/twilio/sms -``` - -```typescript -// Configure webhook URL based on environment -function getWebhookBaseUrl(): string { - if (process.env.NODE_ENV === 'production') { - return process.env.APP_URL!; // https://yourapp.com - } - // In development, use ngrok or similar tunnel - return process.env.NGROK_URL || 'https://your-tunnel.ngrok.io'; -} -``` - -## Next.js Webhook Handlers - -```typescript -// app/api/webhooks/twilio/sms/route.ts -import twilio from 'twilio'; -import { validateRequest } from 'twilio'; -import { headers } from 'next/headers'; - -export async function POST(req: Request) { - const body = await req.text(); - const headersList = await headers(); - - // Parse the URL-encoded body - const params = Object.fromEntries(new URLSearchParams(body)); - - // Validate the request signature - const signature = headersList.get('x-twilio-signature') || ''; - const url = `${process.env.APP_URL}/api/webhooks/twilio/sms`; - const isValid = validateRequest( - process.env.TWILIO_AUTH_TOKEN!, - signature, - url, - params - ); - - if (!isValid) { - return new Response('Invalid signature', { status: 403 }); - } - - // Build TwiML response - const response = new twilio.twiml.MessagingResponse(); - response.message(`Received: ${params.Body}`); - - return new Response(response.toString(), { - status: 200, - headers: { 'Content-Type': 'text/xml' }, - }); -} -``` - -## Logging Webhook Events - -- **Log all webhook events** for debugging and audit purposes. Include the SID, event type, and relevant parameters. - -```typescript -function logTwilioWebhook(type: string, params: Record<string, string>) { - console.log(JSON.stringify({ - level: 'info', - source: 'twilio_webhook', - type, - messageSid: params.MessageSid || params.CallSid || undefined, - from: params.From, - to: params.To, - status: params.MessageStatus || params.CallStatus || undefined, - timestamp: new Date().toISOString(), - })); -} -``` diff --git a/plugins/twilio/scripts/check-twilio-credentials.sh b/plugins/twilio/scripts/check-twilio-credentials.sh deleted file mode 100755 index dbf45a6..0000000 --- a/plugins/twilio/scripts/check-twilio-credentials.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# -# Pre-commit hook: Prevent committing live Twilio credentials -# -# This script scans staged files for Twilio Auth Tokens, API Key Secrets, -# and other sensitive credentials that should never be committed. -# Account SIDs (AC*) are informational and allowed — Auth Tokens are not. -# -# Exit codes: -# 0 — No credentials found, commit is safe -# 1 — Credentials detected, commit is blocked - -set -euo pipefail - -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -NC='\033[0m' # No Color - -# Patterns for sensitive Twilio credentials that must NOT be committed -# Auth tokens are 32-character hex strings, API key secrets are similar -BLOCKED_PATTERNS=( - # Twilio Auth Token (32 hex chars, often assigned to a variable) - 'TWILIO_AUTH_TOKEN\s*=\s*["\x27][a-f0-9]{32}["\x27]' - 'authToken\s*[:=]\s*["\x27][a-f0-9]{32}["\x27]' - 'AUTH_TOKEN\s*=\s*["\x27][a-f0-9]{32}["\x27]' - # Twilio API Key Secret - 'TWILIO_API_KEY_SECRET\s*=\s*["\x27][a-zA-Z0-9]{32}["\x27]' - 'apiKeySecret\s*[:=]\s*["\x27][a-zA-Z0-9]{32}["\x27]' -) - -# Files to exclude from scanning -EXCLUDE_PATTERNS=( - '*.md' - '*.mdc' - 'check-twilio-credentials.sh' - '*.lock' - '*.png' - '*.jpg' - '*.svg' - '*.ico' - '*.env.example' - '*.env.sample' -) - -found_credentials=0 -flagged_files="" - -# Get list of staged files (only added/modified, not deleted) -staged_files=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true) - -if [ -z "$staged_files" ]; then - exit 0 -fi - -echo -e "${YELLOW}Twilio Credential Check:${NC} Scanning staged files for Twilio credentials..." - -for pattern in "${BLOCKED_PATTERNS[@]}"; do - while IFS= read -r file; do - # Skip excluded file patterns - skip=false - for exclude in "${EXCLUDE_PATTERNS[@]}"; do - if [[ "$file" == $exclude ]]; then - skip=true - break - fi - done - if [ "$skip" = true ]; then - continue - fi - - # Check file content from the staging area - if git show ":$file" 2>/dev/null | grep -qE "$pattern"; then - matches=$(git show ":$file" | grep -nE "$pattern" || true) - if [ -n "$matches" ]; then - echo -e "${RED}BLOCKED:${NC} Twilio credential found in ${YELLOW}${file}${NC}:" - echo "$matches" | while IFS= read -r line; do - # Mask the credential value for security - masked=$(echo "$line" | sed -E "s/[a-f0-9]{32}/***REDACTED***/g") - echo " $masked" - done - found_credentials=1 - flagged_files="$flagged_files\n - $file" - fi - fi - done <<< "$staged_files" -done - -if [ "$found_credentials" -ne 0 ]; then - echo "" - echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${RED}║ COMMIT BLOCKED: Twilio credentials detected! ║${NC}" - echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}" - echo "" - echo -e "Affected files:${flagged_files}" - echo "" - echo -e "To fix this:" - echo -e " 1. Replace credentials with environment variable references" - echo -e " e.g., ${GREEN}process.env.TWILIO_AUTH_TOKEN${NC}" - echo -e " 2. Store credentials in your .env file (add .env to .gitignore)" - echo -e " 3. Use the Twilio CLI for local development" - echo "" - echo -e "To bypass this check (NOT recommended):" - echo -e " ${YELLOW}git commit --no-verify${NC}" - echo "" - exit 1 -fi - -echo -e "${GREEN}✓${NC} No Twilio credentials found in staged files." -exit 0