Initial commit
This commit is contained in:
34
.claude-plugin/plugin.json
Normal file
34
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "claudekit-skills",
|
||||
"description": "ClaudeKit Skills - Comprehensive collection of specialized agent skills, commands, and agents for authentication, AI/ML, web development, cloud platforms, databases, debugging, documentation, problem-solving, and more",
|
||||
"version": "main",
|
||||
"author": {
|
||||
"name": "mrgoonie",
|
||||
"url": "https://github.com/mrgoonie"
|
||||
},
|
||||
"skills": [
|
||||
"./skills/ai-multimodal",
|
||||
"./skills/better-auth",
|
||||
"./skills/chrome-devtools",
|
||||
"./skills/claude-code",
|
||||
"./skills/code-review",
|
||||
"./skills/common",
|
||||
"./skills/databases",
|
||||
"./skills/debugging",
|
||||
"./skills/devops",
|
||||
"./skills/docs-seeker",
|
||||
"./skills/document-skills",
|
||||
"./skills/google-adk-python",
|
||||
"./skills/mcp-builder",
|
||||
"./skills/mcp-management",
|
||||
"./skills/media-processing",
|
||||
"./skills/problem-solving",
|
||||
"./skills/repomix",
|
||||
"./skills/sequential-thinking",
|
||||
"./skills/shopify",
|
||||
"./skills/skill-creator",
|
||||
"./skills/template-skill",
|
||||
"./skills/ui-styling",
|
||||
"./skills/web-frameworks"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# claudekit-skills
|
||||
|
||||
ClaudeKit Skills - Comprehensive collection of specialized agent skills, commands, and agents for authentication, AI/ML, web development, cloud platforms, databases, debugging, documentation, problem-solving, and more
|
||||
1813
plugin.lock.json
Normal file
1813
plugin.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
97
skills/ai-multimodal/.env.example
Normal file
97
skills/ai-multimodal/.env.example
Normal file
@@ -0,0 +1,97 @@
|
||||
# Google Gemini API Configuration
|
||||
|
||||
# ============================================================================
|
||||
# OPTION 1: Google AI Studio (Default - Recommended for most users)
|
||||
# ============================================================================
|
||||
# Get your API key: https://aistudio.google.com/apikey
|
||||
GEMINI_API_KEY=your_api_key_here
|
||||
|
||||
# ============================================================================
|
||||
# OPTION 2: Vertex AI (Google Cloud Platform)
|
||||
# ============================================================================
|
||||
# Uncomment these lines to use Vertex AI instead of Google AI Studio
|
||||
# GEMINI_USE_VERTEX=true
|
||||
# VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
# VERTEX_LOCATION=us-central1
|
||||
|
||||
# ============================================================================
|
||||
# Model Selection (Optional)
|
||||
# ============================================================================
|
||||
# Override default model for specific tasks
|
||||
# Default: gemini-2.5-flash for most tasks
|
||||
# GEMINI_MODEL=gemini-2.5-flash
|
||||
# GEMINI_IMAGE_GEN_MODEL=gemini-2.5-flash-image
|
||||
|
||||
# ============================================================================
|
||||
# Rate Limiting Configuration (Optional)
|
||||
# ============================================================================
|
||||
# Requests per minute limit (adjust based on your tier)
|
||||
# GEMINI_RPM_LIMIT=15
|
||||
|
||||
# Tokens per minute limit
|
||||
# GEMINI_TPM_LIMIT=4000000
|
||||
|
||||
# Requests per day limit
|
||||
# GEMINI_RPD_LIMIT=1500
|
||||
|
||||
# ============================================================================
|
||||
# Processing Options (Optional)
|
||||
# ============================================================================
|
||||
# Video resolution mode: default or low-res
|
||||
# low-res uses ~100 tokens/second vs ~300 for default
|
||||
# GEMINI_VIDEO_RESOLUTION=default
|
||||
|
||||
# Audio quality: default (16 Kbps mono, auto-downsampled)
|
||||
# GEMINI_AUDIO_QUALITY=default
|
||||
|
||||
# PDF processing mode: inline (<20MB) or file-api (>20MB, automatic)
|
||||
# GEMINI_PDF_MODE=auto
|
||||
|
||||
# ============================================================================
|
||||
# Retry Configuration (Optional)
|
||||
# ============================================================================
|
||||
# Maximum retry attempts for failed requests
|
||||
# GEMINI_MAX_RETRIES=3
|
||||
|
||||
# Initial retry delay in seconds (uses exponential backoff)
|
||||
# GEMINI_RETRY_DELAY=1
|
||||
|
||||
# ============================================================================
|
||||
# Output Configuration (Optional)
|
||||
# ============================================================================
|
||||
# Default output directory for generated images
|
||||
# OUTPUT_DIR=./output
|
||||
|
||||
# Image output format (png or jpeg)
|
||||
# IMAGE_FORMAT=png
|
||||
|
||||
# Image quality for JPEG (1-100)
|
||||
# IMAGE_QUALITY=95
|
||||
|
||||
# ============================================================================
|
||||
# Context Caching (Optional)
|
||||
# ============================================================================
|
||||
# Enable context caching for repeated queries on same file
|
||||
# GEMINI_ENABLE_CACHING=true
|
||||
|
||||
# Cache TTL in seconds (default: 1800 = 30 minutes)
|
||||
# GEMINI_CACHE_TTL=1800
|
||||
|
||||
# ============================================================================
|
||||
# Logging (Optional)
|
||||
# ============================================================================
|
||||
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# LOG_LEVEL=INFO
|
||||
|
||||
# Log file path
|
||||
# LOG_FILE=./logs/gemini.log
|
||||
|
||||
# ============================================================================
|
||||
# Notes
|
||||
# ============================================================================
|
||||
# 1. Never commit API keys to version control
|
||||
# 2. Add .env to .gitignore
|
||||
# 3. API keys can be restricted in Google Cloud Console
|
||||
# 4. Monitor usage at: https://aistudio.google.com/apikey
|
||||
# 5. Free tier limits: 15 RPM, 1M-4M TPM, 1,500 RPD
|
||||
# 6. Vertex AI requires GCP authentication via gcloud CLI
|
||||
357
skills/ai-multimodal/SKILL.md
Normal file
357
skills/ai-multimodal/SKILL.md
Normal file
@@ -0,0 +1,357 @@
|
||||
---
|
||||
name: ai-multimodal
|
||||
description: Process and generate multimedia content using Google Gemini API. Capabilities include analyze audio files (transcription with timestamps, summarization, speech understanding, music/sound analysis up to 9.5 hours), understand images (captioning, object detection, OCR, visual Q&A, segmentation), process videos (scene detection, Q&A, temporal analysis, YouTube URLs, up to 6 hours), extract from documents (PDF tables, forms, charts, diagrams, multi-page), generate images (text-to-image, editing, composition, refinement). Use when working with audio/video files, analyzing images or screenshots, processing PDF documents, extracting structured data from media, creating images from text prompts, or implementing multimodal AI features. Supports multiple models (Gemini 2.5/2.0) with context windows up to 2M tokens.
|
||||
license: MIT
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
---
|
||||
|
||||
# AI Multimodal Processing Skill
|
||||
|
||||
Process audio, images, videos, documents, and generate images using Google Gemini's multimodal API. Unified interface for all multimedia content understanding and generation.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### Audio Processing
|
||||
- Transcription with timestamps (up to 9.5 hours)
|
||||
- Audio summarization and analysis
|
||||
- Speech understanding and speaker identification
|
||||
- Music and environmental sound analysis
|
||||
- Text-to-speech generation with controllable voice
|
||||
|
||||
### Image Understanding
|
||||
- Image captioning and description
|
||||
- Object detection with bounding boxes (2.0+)
|
||||
- Pixel-level segmentation (2.5+)
|
||||
- Visual question answering
|
||||
- Multi-image comparison (up to 3,600 images)
|
||||
- OCR and text extraction
|
||||
|
||||
### Video Analysis
|
||||
- Scene detection and summarization
|
||||
- Video Q&A with temporal understanding
|
||||
- Transcription with visual descriptions
|
||||
- YouTube URL support
|
||||
- Long video processing (up to 6 hours)
|
||||
- Frame-level analysis
|
||||
|
||||
### Document Extraction
|
||||
- Native PDF vision processing (up to 1,000 pages)
|
||||
- Table and form extraction
|
||||
- Chart and diagram analysis
|
||||
- Multi-page document understanding
|
||||
- Structured data output (JSON schema)
|
||||
- Format conversion (PDF to HTML/JSON)
|
||||
|
||||
### Image Generation
|
||||
- Text-to-image generation
|
||||
- Image editing and modification
|
||||
- Multi-image composition (up to 3 images)
|
||||
- Iterative refinement
|
||||
- Multiple aspect ratios (1:1, 16:9, 9:16, 4:3, 3:4)
|
||||
- Controllable style and quality
|
||||
|
||||
## Capability Matrix
|
||||
|
||||
| Task | Audio | Image | Video | Document | Generation |
|
||||
|------|:-----:|:-----:|:-----:|:--------:|:----------:|
|
||||
| Transcription | ✓ | - | ✓ | - | - |
|
||||
| Summarization | ✓ | ✓ | ✓ | ✓ | - |
|
||||
| Q&A | ✓ | ✓ | ✓ | ✓ | - |
|
||||
| Object Detection | - | ✓ | ✓ | - | - |
|
||||
| Text Extraction | - | ✓ | - | ✓ | - |
|
||||
| Structured Output | ✓ | ✓ | ✓ | ✓ | - |
|
||||
| Creation | TTS | - | - | - | ✓ |
|
||||
| Timestamps | ✓ | - | ✓ | - | - |
|
||||
| Segmentation | - | ✓ | - | - | - |
|
||||
|
||||
## Model Selection Guide
|
||||
|
||||
### Gemini 2.5 Series (Recommended)
|
||||
- **gemini-2.5-pro**: Highest quality, all features, 1M-2M context
|
||||
- **gemini-2.5-flash**: Best balance, all features, 1M-2M context
|
||||
- **gemini-2.5-flash-lite**: Lightweight, segmentation support
|
||||
- **gemini-2.5-flash-image**: Image generation only
|
||||
|
||||
### Gemini 2.0 Series
|
||||
- **gemini-2.0-flash**: Fast processing, object detection
|
||||
- **gemini-2.0-flash-lite**: Lightweight option
|
||||
|
||||
### Feature Requirements
|
||||
- **Segmentation**: Requires 2.5+ models
|
||||
- **Object Detection**: Requires 2.0+ models
|
||||
- **Multi-video**: Requires 2.5+ models
|
||||
- **Image Generation**: Requires flash-image model
|
||||
|
||||
### Context Windows
|
||||
- **2M tokens**: ~6 hours video (low-res) or ~2 hours (default)
|
||||
- **1M tokens**: ~3 hours video (low-res) or ~1 hour (default)
|
||||
- **Audio**: 32 tokens/second (1 min = 1,920 tokens)
|
||||
- **PDF**: 258 tokens/page (fixed)
|
||||
- **Image**: 258-1,548 tokens based on size
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**API Key Setup**: Supports both Google AI Studio and Vertex AI.
|
||||
|
||||
The skill checks for `GEMINI_API_KEY` in this order:
|
||||
1. Process environment: `export GEMINI_API_KEY="your-key"`
|
||||
2. Project root: `.env`
|
||||
3. `.claude/.env`
|
||||
4. `.claude/skills/.env`
|
||||
5. `.claude/skills/ai-multimodal/.env`
|
||||
|
||||
**Get API key**: https://aistudio.google.com/apikey
|
||||
|
||||
**For Vertex AI**:
|
||||
```bash
|
||||
export GEMINI_USE_VERTEX=true
|
||||
export VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
export VERTEX_LOCATION=us-central1 # Optional
|
||||
```
|
||||
|
||||
**Install SDK**:
|
||||
```bash
|
||||
pip install google-genai python-dotenv pillow
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Transcribe Audio**:
|
||||
```bash
|
||||
python scripts/gemini_batch_process.py \
|
||||
--files audio.mp3 \
|
||||
--task transcribe \
|
||||
--model gemini-2.5-flash
|
||||
```
|
||||
|
||||
**Analyze Image**:
|
||||
```bash
|
||||
python scripts/gemini_batch_process.py \
|
||||
--files image.jpg \
|
||||
--task analyze \
|
||||
--prompt "Describe this image" \
|
||||
--output docs/assets/<output-name>.md \
|
||||
--model gemini-2.5-flash
|
||||
```
|
||||
|
||||
**Process Video**:
|
||||
```bash
|
||||
python scripts/gemini_batch_process.py \
|
||||
--files video.mp4 \
|
||||
--task analyze \
|
||||
--prompt "Summarize key points with timestamps" \
|
||||
--output docs/assets/<output-name>.md \
|
||||
--model gemini-2.5-flash
|
||||
```
|
||||
|
||||
**Extract from PDF**:
|
||||
```bash
|
||||
python scripts/gemini_batch_process.py \
|
||||
--files document.pdf \
|
||||
--task extract \
|
||||
--prompt "Extract table data as JSON" \
|
||||
--output docs/assets/<output-name>.md \
|
||||
--format json
|
||||
```
|
||||
|
||||
**Generate Image**:
|
||||
```bash
|
||||
python scripts/gemini_batch_process.py \
|
||||
--task generate \
|
||||
--prompt "A futuristic city at sunset" \
|
||||
--output docs/assets/<output-file-name> \
|
||||
--model gemini-2.5-flash-image \
|
||||
--aspect-ratio 16:9
|
||||
```
|
||||
|
||||
**Optimize Media**:
|
||||
```bash
|
||||
# Prepare large video for processing
|
||||
python scripts/media_optimizer.py \
|
||||
--input large-video.mp4 \
|
||||
--output docs/assets/<output-file-name> \
|
||||
--target-size 100MB
|
||||
|
||||
# Batch optimize multiple files
|
||||
python scripts/media_optimizer.py \
|
||||
--input-dir ./videos \
|
||||
--output-dir docs/assets/optimized \
|
||||
--quality 85
|
||||
```
|
||||
|
||||
**Convert Documents to Markdown**:
|
||||
```bash
|
||||
# Convert to PDF
|
||||
python scripts/document_converter.py \
|
||||
--input document.docx \
|
||||
--output docs/assets/document.md
|
||||
|
||||
# Extract pages
|
||||
python scripts/document_converter.py \
|
||||
--input large.pdf \
|
||||
--output docs/assets/chapter1.md \
|
||||
--pages 1-20
|
||||
```
|
||||
|
||||
## Supported Formats
|
||||
|
||||
### Audio
|
||||
- WAV, MP3, AAC, FLAC, OGG Vorbis, AIFF
|
||||
- Max 9.5 hours per request
|
||||
- Auto-downsampled to 16 Kbps mono
|
||||
|
||||
### Images
|
||||
- PNG, JPEG, WEBP, HEIC, HEIF
|
||||
- Max 3,600 images per request
|
||||
- Resolution: ≤384px = 258 tokens, larger = tiled
|
||||
|
||||
### Video
|
||||
- MP4, MPEG, MOV, AVI, FLV, MPG, WebM, WMV, 3GPP
|
||||
- Max 6 hours (low-res) or 2 hours (default)
|
||||
- YouTube URLs supported (public only)
|
||||
|
||||
### Documents
|
||||
- PDF only for vision processing
|
||||
- Max 1,000 pages
|
||||
- TXT, HTML, Markdown supported (text-only)
|
||||
|
||||
### Size Limits
|
||||
- **Inline**: <20MB total request
|
||||
- **File API**: 2GB per file, 20GB project quota
|
||||
- **Retention**: 48 hours auto-delete
|
||||
|
||||
## Reference Navigation
|
||||
|
||||
For detailed implementation guidance, see:
|
||||
|
||||
### Audio Processing
|
||||
- `references/audio-processing.md` - Transcription, analysis, TTS
|
||||
- Timestamp handling and segment analysis
|
||||
- Multi-speaker identification
|
||||
- Non-speech audio analysis
|
||||
- Text-to-speech generation
|
||||
|
||||
### Image Understanding
|
||||
- `references/vision-understanding.md` - Captioning, detection, OCR
|
||||
- Object detection and localization
|
||||
- Pixel-level segmentation
|
||||
- Visual question answering
|
||||
- Multi-image comparison
|
||||
|
||||
### Video Analysis
|
||||
- `references/video-analysis.md` - Scene detection, temporal understanding
|
||||
- YouTube URL processing
|
||||
- Timestamp-based queries
|
||||
- Video clipping and FPS control
|
||||
- Long video optimization
|
||||
|
||||
### Document Extraction
|
||||
- `references/document-extraction.md` - PDF processing, structured output
|
||||
- Table and form extraction
|
||||
- Chart and diagram analysis
|
||||
- JSON schema validation
|
||||
- Multi-page handling
|
||||
|
||||
### Image Generation
|
||||
- `references/image-generation.md` - Text-to-image, editing
|
||||
- Prompt engineering strategies
|
||||
- Image editing and composition
|
||||
- Aspect ratio selection
|
||||
- Safety settings
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
### Token Costs
|
||||
**Input Pricing**:
|
||||
- Gemini 2.5 Flash: $1.00/1M input, $0.10/1M output
|
||||
- Gemini 2.5 Pro: $3.00/1M input, $12.00/1M output
|
||||
- Gemini 1.5 Flash: $0.70/1M input, $0.175/1M output
|
||||
|
||||
**Token Rates**:
|
||||
- Audio: 32 tokens/second (1 min = 1,920 tokens)
|
||||
- Video: ~300 tokens/second (default) or ~100 (low-res)
|
||||
- PDF: 258 tokens/page (fixed)
|
||||
- Image: 258-1,548 tokens based on size
|
||||
|
||||
**TTS Pricing**:
|
||||
- Flash TTS: $10/1M tokens
|
||||
- Pro TTS: $20/1M tokens
|
||||
|
||||
### Best Practices
|
||||
1. Use `gemini-2.5-flash` for most tasks (best price/performance)
|
||||
2. Use File API for files >20MB or repeated queries
|
||||
3. Optimize media before upload (see `media_optimizer.py`)
|
||||
4. Process specific segments instead of full videos
|
||||
5. Use lower FPS for static content
|
||||
6. Implement context caching for repeated queries
|
||||
7. Batch process multiple files in parallel
|
||||
|
||||
## Rate Limits
|
||||
|
||||
**Free Tier**:
|
||||
- 10-15 RPM (requests per minute)
|
||||
- 1M-4M TPM (tokens per minute)
|
||||
- 1,500 RPD (requests per day)
|
||||
|
||||
**YouTube Limits**:
|
||||
- Free tier: 8 hours/day
|
||||
- Paid tier: No length limits
|
||||
- Public videos only
|
||||
|
||||
**Storage Limits**:
|
||||
- 20GB per project
|
||||
- 2GB per file
|
||||
- 48-hour retention
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common errors and solutions:
|
||||
- **400**: Invalid format/size - validate before upload
|
||||
- **401**: Invalid API key - check configuration
|
||||
- **403**: Permission denied - verify API key restrictions
|
||||
- **404**: File not found - ensure file uploaded and active
|
||||
- **429**: Rate limit exceeded - implement exponential backoff
|
||||
- **500**: Server error - retry with backoff
|
||||
|
||||
## Scripts Overview
|
||||
|
||||
All scripts support unified API key detection and error handling:
|
||||
|
||||
**gemini_batch_process.py**: Batch process multiple media files
|
||||
- Supports all modalities (audio, image, video, PDF)
|
||||
- Progress tracking and error recovery
|
||||
- Output formats: JSON, Markdown, CSV
|
||||
- Rate limiting and retry logic
|
||||
- Dry-run mode
|
||||
|
||||
**media_optimizer.py**: Prepare media for Gemini API
|
||||
- Compress videos/audio for size limits
|
||||
- Resize images appropriately
|
||||
- Split long videos into chunks
|
||||
- Format conversion
|
||||
- Quality vs size optimization
|
||||
|
||||
**document_converter.py**: Convert documents to PDF
|
||||
- Convert DOCX, XLSX, PPTX to PDF
|
||||
- Extract page ranges
|
||||
- Optimize PDFs for Gemini
|
||||
- Extract images from PDFs
|
||||
- Batch conversion support
|
||||
|
||||
Run any script with `--help` for detailed usage.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Audio API Docs](https://ai.google.dev/gemini-api/docs/audio)
|
||||
- [Image API Docs](https://ai.google.dev/gemini-api/docs/image-understanding)
|
||||
- [Video API Docs](https://ai.google.dev/gemini-api/docs/video-understanding)
|
||||
- [Document API Docs](https://ai.google.dev/gemini-api/docs/document-processing)
|
||||
- [Image Gen Docs](https://ai.google.dev/gemini-api/docs/image-generation)
|
||||
- [Get API Key](https://aistudio.google.com/apikey)
|
||||
- [Pricing](https://ai.google.dev/pricing)
|
||||
373
skills/ai-multimodal/references/audio-processing.md
Normal file
373
skills/ai-multimodal/references/audio-processing.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# Audio Processing Reference
|
||||
|
||||
Comprehensive guide for audio analysis and speech generation using Gemini API.
|
||||
|
||||
## Audio Understanding
|
||||
|
||||
### Supported Formats
|
||||
|
||||
| Format | MIME Type | Best Use |
|
||||
|--------|-----------|----------|
|
||||
| WAV | `audio/wav` | Uncompressed, highest quality |
|
||||
| MP3 | `audio/mp3` | Compressed, widely compatible |
|
||||
| AAC | `audio/aac` | Compressed, good quality |
|
||||
| FLAC | `audio/flac` | Lossless compression |
|
||||
| OGG Vorbis | `audio/ogg` | Open format |
|
||||
| AIFF | `audio/aiff` | Apple format |
|
||||
|
||||
### Specifications
|
||||
|
||||
- **Maximum length**: 9.5 hours per request
|
||||
- **Multiple files**: Unlimited count, combined max 9.5 hours
|
||||
- **Token rate**: 32 tokens/second (1 minute = 1,920 tokens)
|
||||
- **Processing**: Auto-downsampled to 16 Kbps mono
|
||||
- **File size limits**:
|
||||
- Inline: 20 MB max total request
|
||||
- File API: 2 GB per file, 20 GB project quota
|
||||
- Retention: 48 hours auto-delete
|
||||
|
||||
## Transcription
|
||||
|
||||
### Basic Transcription
|
||||
|
||||
```python
|
||||
from google import genai
|
||||
import os
|
||||
|
||||
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
|
||||
|
||||
# Upload audio
|
||||
myfile = client.files.upload(file='meeting.mp3')
|
||||
|
||||
# Transcribe
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Generate a transcript of the speech.', myfile]
|
||||
)
|
||||
print(response.text)
|
||||
```
|
||||
|
||||
### With Timestamps
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Generate transcript with timestamps in MM:SS format.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
### Multi-Speaker Identification
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Transcribe with speaker labels. Format: [Speaker 1], [Speaker 2], etc.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
### Segment-Specific Transcription
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Transcribe only the segment from 02:30 to 05:15.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
## Audio Analysis
|
||||
|
||||
### Summarization
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Summarize key points in 5 bullets with timestamps.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
### Non-Speech Audio Analysis
|
||||
|
||||
```python
|
||||
# Music analysis
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Identify the musical instruments and genre.', myfile]
|
||||
)
|
||||
|
||||
# Environmental sounds
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Identify all sounds: voices, music, ambient noise.', myfile]
|
||||
)
|
||||
|
||||
# Birdsong identification
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Identify bird species based on their calls.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
### Timestamp-Based Analysis
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['What is discussed from 10:30 to 15:45? Provide key points.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
## Input Methods
|
||||
|
||||
### File Upload (>20MB or Reuse)
|
||||
|
||||
```python
|
||||
# Upload once, use multiple times
|
||||
myfile = client.files.upload(file='large-audio.mp3')
|
||||
|
||||
# First query
|
||||
response1 = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Transcribe this', myfile]
|
||||
)
|
||||
|
||||
# Second query (reuses same file)
|
||||
response2 = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Summarize this', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
### Inline Data (<20MB)
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
|
||||
with open('small-audio.mp3', 'rb') as f:
|
||||
audio_bytes = f.read()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Describe this audio',
|
||||
types.Part.from_bytes(data=audio_bytes, mime_type='audio/mp3')
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Speech Generation (TTS)
|
||||
|
||||
### Available Models
|
||||
|
||||
| Model | Quality | Speed | Cost/1M tokens |
|
||||
|-------|---------|-------|----------------|
|
||||
| `gemini-2.5-flash-native-audio-preview-09-2025` | High | Fast | $10 |
|
||||
| `gemini-2.5-pro` TTS mode | Premium | Slower | $20 |
|
||||
|
||||
### Basic TTS
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-native-audio-preview-09-2025',
|
||||
contents='Generate audio: Welcome to today\'s episode.'
|
||||
)
|
||||
|
||||
# Save audio
|
||||
with open('output.wav', 'wb') as f:
|
||||
f.write(response.audio_data)
|
||||
```
|
||||
|
||||
### Controllable Voice Style
|
||||
|
||||
```python
|
||||
# Professional tone
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-native-audio-preview-09-2025',
|
||||
contents='Generate audio in a professional, clear tone: Welcome to our quarterly earnings call.'
|
||||
)
|
||||
|
||||
# Casual and friendly
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-native-audio-preview-09-2025',
|
||||
contents='Generate audio in a friendly, conversational tone: Hey there! Let\'s dive into today\'s topic.'
|
||||
)
|
||||
|
||||
# Narrative style
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-native-audio-preview-09-2025',
|
||||
contents='Generate audio in a narrative, storytelling tone: Once upon a time, in a land far away...'
|
||||
)
|
||||
```
|
||||
|
||||
### Voice Control Parameters
|
||||
|
||||
- **Style**: Professional, casual, narrative, conversational
|
||||
- **Pace**: Slow, normal, fast
|
||||
- **Tone**: Friendly, serious, enthusiastic
|
||||
- **Accent**: Natural language control (e.g., "British accent", "Southern drawl")
|
||||
|
||||
## Best Practices
|
||||
|
||||
### File Management
|
||||
|
||||
1. Use File API for files >20MB
|
||||
2. Use File API for repeated queries (saves tokens)
|
||||
3. Files auto-delete after 48 hours
|
||||
4. Clean up manually when done:
|
||||
```python
|
||||
client.files.delete(name=myfile.name)
|
||||
```
|
||||
|
||||
### Prompt Engineering
|
||||
|
||||
**Effective prompts**:
|
||||
- "Transcribe from 02:30 to 03:29 in MM:SS format"
|
||||
- "Identify speakers and extract dialogue with timestamps"
|
||||
- "Summarize key points with relevant timestamps"
|
||||
- "Transcribe and analyze sentiment for each speaker"
|
||||
|
||||
**Context improves accuracy**:
|
||||
- "This is a medical interview - use appropriate terminology"
|
||||
- "Transcribe this legal deposition with precise terminology"
|
||||
- "This is a technical podcast about machine learning"
|
||||
|
||||
**Combined tasks**:
|
||||
- "Transcribe and summarize in bullet points"
|
||||
- "Extract key quotes with timestamps and speaker labels"
|
||||
- "Transcribe and identify action items with timestamps"
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
**Token calculation**:
|
||||
- 1 minute audio = 1,920 tokens
|
||||
- 1 hour audio = 115,200 tokens
|
||||
- 9.5 hours = 1,094,400 tokens
|
||||
|
||||
**Model selection**:
|
||||
- Use `gemini-2.5-flash` ($1/1M tokens) for most tasks
|
||||
- Upgrade to `gemini-2.5-pro` ($3/1M tokens) for complex analysis
|
||||
- For high-volume: `gemini-1.5-flash` ($0.70/1M tokens)
|
||||
|
||||
**Reduce costs**:
|
||||
- Process only relevant segments using timestamps
|
||||
- Use lower-quality audio when possible
|
||||
- Batch multiple short files in one request
|
||||
- Cache context for repeated queries
|
||||
|
||||
### Error Handling
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def transcribe_with_retry(file_path, max_retries=3):
|
||||
"""Transcribe audio with exponential backoff retry"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
myfile = client.files.upload(file=file_path)
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Transcribe with timestamps', myfile]
|
||||
)
|
||||
return response.text
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
wait_time = 2 ** attempt
|
||||
print(f"Retry {attempt + 1} after {wait_time}s")
|
||||
time.sleep(wait_time)
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Meeting Transcription
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Transcribe this meeting with:
|
||||
1. Speaker labels
|
||||
2. Timestamps for topic changes
|
||||
3. Action items highlighted
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Podcast Summary
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Create podcast summary with:
|
||||
1. Main topics with timestamps
|
||||
2. Key quotes from each speaker
|
||||
3. Recommended episode highlights
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Interview Analysis
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Analyze interview:
|
||||
1. Questions asked with timestamps
|
||||
2. Key responses from interviewee
|
||||
3. Overall sentiment and tone
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Content Verification
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Verify audio content:
|
||||
1. Check for specific keywords or phrases
|
||||
2. Identify any compliance issues
|
||||
3. Note any concerning statements with timestamps
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Multilingual Transcription
|
||||
|
||||
```python
|
||||
# Gemini auto-detects language
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Transcribe this audio and translate to English if needed.', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
## Token Costs
|
||||
|
||||
**Audio Input** (32 tokens/second):
|
||||
- 1 minute = 1,920 tokens
|
||||
- 10 minutes = 19,200 tokens
|
||||
- 1 hour = 115,200 tokens
|
||||
- 9.5 hours = 1,094,400 tokens
|
||||
|
||||
**Example costs** (Gemini 2.5 Flash at $1/1M):
|
||||
- 1 hour audio: 115,200 tokens = $0.12
|
||||
- Full day podcast (8 hours): 921,600 tokens = $0.92
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum 9.5 hours per request
|
||||
- Auto-downsampled to 16 Kbps mono (quality loss)
|
||||
- Files expire after 48 hours
|
||||
- No real-time streaming support
|
||||
- Non-speech audio less accurate than speech
|
||||
558
skills/ai-multimodal/references/image-generation.md
Normal file
558
skills/ai-multimodal/references/image-generation.md
Normal file
@@ -0,0 +1,558 @@
|
||||
# Image Generation Reference
|
||||
|
||||
Comprehensive guide for image creation, editing, and composition using Gemini API.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Text-to-Image**: Generate images from text prompts
|
||||
- **Image Editing**: Modify existing images with text instructions
|
||||
- **Multi-Image Composition**: Combine up to 3 images
|
||||
- **Iterative Refinement**: Refine images conversationally
|
||||
- **Aspect Ratios**: Multiple formats (1:1, 16:9, 9:16, 4:3, 3:4)
|
||||
- **Style Control**: Control artistic style and quality
|
||||
- **Text in Images**: Limited text rendering (max 25 chars)
|
||||
|
||||
## Model
|
||||
|
||||
**gemini-2.5-flash-image** - Specialized for image generation
|
||||
- Input tokens: 65,536
|
||||
- Output tokens: 32,768
|
||||
- Knowledge cutoff: June 2025
|
||||
- Supports: Text and image inputs, image outputs
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Generation
|
||||
|
||||
```python
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
import os
|
||||
|
||||
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='A serene mountain landscape at sunset with snow-capped peaks',
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='16:9'
|
||||
)
|
||||
)
|
||||
|
||||
# Save image
|
||||
for i, part in enumerate(response.candidates[0].content.parts):
|
||||
if part.inline_data:
|
||||
with open(f'output-{i}.png', 'wb') as f:
|
||||
f.write(part.inline_data.data)
|
||||
```
|
||||
|
||||
## Aspect Ratios
|
||||
|
||||
| Ratio | Resolution | Use Case | Token Cost |
|
||||
|-------|-----------|----------|------------|
|
||||
| 1:1 | 1024×1024 | Social media, avatars | 1290 |
|
||||
| 16:9 | 1344×768 | Landscapes, banners | 1290 |
|
||||
| 9:16 | 768×1344 | Mobile, portraits | 1290 |
|
||||
| 4:3 | 1152×896 | Traditional media | 1290 |
|
||||
| 3:4 | 896×1152 | Vertical posters | 1290 |
|
||||
|
||||
All ratios cost the same: 1,290 tokens per image.
|
||||
|
||||
## Response Modalities
|
||||
|
||||
### Image Only
|
||||
|
||||
```python
|
||||
config = types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='1:1'
|
||||
)
|
||||
```
|
||||
|
||||
### Text Only (No Image)
|
||||
|
||||
```python
|
||||
config = types.GenerateContentConfig(
|
||||
response_modalities=['text']
|
||||
)
|
||||
# Returns text description instead of generating image
|
||||
```
|
||||
|
||||
### Both Image and Text
|
||||
|
||||
```python
|
||||
config = types.GenerateContentConfig(
|
||||
response_modalities=['image', 'text'],
|
||||
aspect_ratio='16:9'
|
||||
)
|
||||
# Returns both generated image and description
|
||||
```
|
||||
|
||||
## Image Editing
|
||||
|
||||
### Modify Existing Image
|
||||
|
||||
```python
|
||||
import PIL.Image
|
||||
|
||||
# Load original
|
||||
img = PIL.Image.open('original.png')
|
||||
|
||||
# Edit with instructions
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=[
|
||||
'Add a red balloon floating in the sky',
|
||||
img
|
||||
],
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='16:9'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Style Transfer
|
||||
|
||||
```python
|
||||
img = PIL.Image.open('photo.jpg')
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=[
|
||||
'Transform this into an oil painting style',
|
||||
img
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Object Addition/Removal
|
||||
|
||||
```python
|
||||
# Add object
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=[
|
||||
'Add a vintage car parked on the street',
|
||||
img
|
||||
]
|
||||
)
|
||||
|
||||
# Remove object
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=[
|
||||
'Remove the person on the left side',
|
||||
img
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Multi-Image Composition
|
||||
|
||||
### Combine Multiple Images
|
||||
|
||||
```python
|
||||
img1 = PIL.Image.open('background.png')
|
||||
img2 = PIL.Image.open('foreground.png')
|
||||
img3 = PIL.Image.open('overlay.png')
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=[
|
||||
'Combine these images into a cohesive scene',
|
||||
img1,
|
||||
img2,
|
||||
img3
|
||||
],
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='16:9'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Note**: Recommended maximum 3 input images for best results.
|
||||
|
||||
## Prompt Engineering
|
||||
|
||||
### Effective Prompt Structure
|
||||
|
||||
**Three key elements**:
|
||||
1. **Subject**: What to generate
|
||||
2. **Context**: Environmental setting
|
||||
3. **Style**: Artistic treatment
|
||||
|
||||
**Example**: "A robot [subject] in a futuristic city [context], cyberpunk style with neon lighting [style]"
|
||||
|
||||
### Quality Modifiers
|
||||
|
||||
**Technical terms**:
|
||||
- "4K", "8K", "high resolution"
|
||||
- "HDR", "high dynamic range"
|
||||
- "professional photography"
|
||||
- "studio lighting"
|
||||
- "ultra detailed"
|
||||
|
||||
**Camera settings**:
|
||||
- "35mm lens", "50mm lens"
|
||||
- "shallow depth of field"
|
||||
- "wide angle shot"
|
||||
- "macro photography"
|
||||
- "golden hour lighting"
|
||||
|
||||
### Style Keywords
|
||||
|
||||
**Art styles**:
|
||||
- "oil painting", "watercolor", "sketch"
|
||||
- "digital art", "concept art"
|
||||
- "photorealistic", "hyperrealistic"
|
||||
- "minimalist", "abstract"
|
||||
- "cyberpunk", "steampunk", "fantasy"
|
||||
|
||||
**Mood and atmosphere**:
|
||||
- "dramatic lighting", "soft lighting"
|
||||
- "moody", "bright and cheerful"
|
||||
- "mysterious", "whimsical"
|
||||
- "dark and gritty", "pastel colors"
|
||||
|
||||
### Subject Description
|
||||
|
||||
**Be specific**:
|
||||
- ❌ "A cat"
|
||||
- ✅ "A fluffy orange tabby cat with green eyes"
|
||||
|
||||
**Add context**:
|
||||
- ❌ "A building"
|
||||
- ✅ "A modern glass skyscraper reflecting sunset clouds"
|
||||
|
||||
**Include details**:
|
||||
- ❌ "A person"
|
||||
- ✅ "A young woman in a red dress holding an umbrella"
|
||||
|
||||
### Composition and Framing
|
||||
|
||||
**Camera angles**:
|
||||
- "bird's eye view", "aerial shot"
|
||||
- "low angle", "high angle"
|
||||
- "close-up", "wide shot"
|
||||
- "centered composition"
|
||||
- "rule of thirds"
|
||||
|
||||
**Perspective**:
|
||||
- "first person view"
|
||||
- "third person perspective"
|
||||
- "isometric view"
|
||||
- "forced perspective"
|
||||
|
||||
### Text in Images
|
||||
|
||||
**Limitations**:
|
||||
- Maximum 25 characters total
|
||||
- Up to 3 distinct text phrases
|
||||
- Works best with simple text
|
||||
|
||||
**Best practices**:
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='A vintage poster with bold text "EXPLORE" at the top, mountain landscape, retro 1950s style'
|
||||
)
|
||||
```
|
||||
|
||||
**Font control**:
|
||||
- "bold sans-serif title"
|
||||
- "handwritten script"
|
||||
- "vintage letterpress"
|
||||
- "modern minimalist font"
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Iterative Refinement
|
||||
|
||||
```python
|
||||
# Initial generation
|
||||
response1 = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='A futuristic city skyline'
|
||||
)
|
||||
|
||||
# Save first version
|
||||
with open('v1.png', 'wb') as f:
|
||||
f.write(response1.candidates[0].content.parts[0].inline_data.data)
|
||||
|
||||
# Refine
|
||||
img = PIL.Image.open('v1.png')
|
||||
response2 = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=[
|
||||
'Add flying vehicles and neon signs',
|
||||
img
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Negative Prompts (Indirect)
|
||||
|
||||
```python
|
||||
# Instead of "no blur", be specific about what you want
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='A crystal clear, sharp photograph of a diamond ring with perfect focus and high detail'
|
||||
)
|
||||
```
|
||||
|
||||
### Consistent Style Across Images
|
||||
|
||||
```python
|
||||
base_prompt = "Digital art, vibrant colors, cel-shaded style, clean lines"
|
||||
|
||||
prompts = [
|
||||
f"{base_prompt}, a warrior character",
|
||||
f"{base_prompt}, a mage character",
|
||||
f"{base_prompt}, a rogue character"
|
||||
]
|
||||
|
||||
for i, prompt in enumerate(prompts):
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=prompt
|
||||
)
|
||||
# Save each character
|
||||
```
|
||||
|
||||
## Safety Settings
|
||||
|
||||
### Configure Safety Filters
|
||||
|
||||
```python
|
||||
config = types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
safety_settings=[
|
||||
types.SafetySetting(
|
||||
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
threshold=types.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE
|
||||
),
|
||||
types.SafetySetting(
|
||||
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
threshold=types.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Available Categories
|
||||
|
||||
- `HARM_CATEGORY_HATE_SPEECH`
|
||||
- `HARM_CATEGORY_DANGEROUS_CONTENT`
|
||||
- `HARM_CATEGORY_HARASSMENT`
|
||||
- `HARM_CATEGORY_SEXUALLY_EXPLICIT`
|
||||
|
||||
### Thresholds
|
||||
|
||||
- `BLOCK_NONE`: No blocking
|
||||
- `BLOCK_LOW_AND_ABOVE`: Block low probability and above
|
||||
- `BLOCK_MEDIUM_AND_ABOVE`: Block medium and above (default)
|
||||
- `BLOCK_ONLY_HIGH`: Block only high probability
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Marketing Assets
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='''Professional product photography:
|
||||
- Sleek smartphone on minimalist white surface
|
||||
- Dramatic side lighting creating subtle shadows
|
||||
- Shallow depth of field, crisp focus
|
||||
- Clean, modern aesthetic
|
||||
- 4K quality
|
||||
''',
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='4:3'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Concept Art
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='''Fantasy concept art:
|
||||
- Ancient floating islands connected by chains
|
||||
- Waterfalls cascading into clouds below
|
||||
- Magical crystals glowing on the islands
|
||||
- Epic scale, dramatic lighting
|
||||
- Detailed digital painting style
|
||||
''',
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='16:9'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Social Media Graphics
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='''Instagram post design:
|
||||
- Pastel gradient background (pink to blue)
|
||||
- Motivational quote layout
|
||||
- Modern minimalist style
|
||||
- Clean typography
|
||||
- Mobile-friendly composition
|
||||
''',
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='1:1'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Illustration
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='''Children's book illustration:
|
||||
- Friendly cartoon dragon reading a book
|
||||
- Bright, cheerful colors
|
||||
- Soft, rounded shapes
|
||||
- Whimsical forest background
|
||||
- Warm, inviting atmosphere
|
||||
''',
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='4:3'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 5. UI/UX Mockups
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents='''Modern mobile app interface:
|
||||
- Clean dashboard design
|
||||
- Card-based layout
|
||||
- Soft shadows and gradients
|
||||
- Contemporary color scheme (blue and white)
|
||||
- Professional fintech aesthetic
|
||||
''',
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='9:16'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Prompt Quality
|
||||
|
||||
1. **Be specific**: More detail = better results
|
||||
2. **Order matters**: Most important elements first
|
||||
3. **Use examples**: Reference known styles or artists
|
||||
4. **Avoid contradictions**: Don't ask for opposing styles
|
||||
5. **Test and iterate**: Refine prompts based on results
|
||||
|
||||
### File Management
|
||||
|
||||
```python
|
||||
# Save with descriptive names
|
||||
timestamp = int(time.time())
|
||||
filename = f'generated_{timestamp}_{aspect_ratio}.png'
|
||||
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(image_data)
|
||||
```
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
**Token costs**:
|
||||
- 1 image: 1,290 tokens = $0.00129 (Flash Image at $1/1M)
|
||||
- 10 images: 12,900 tokens = $0.0129
|
||||
- 100 images: 129,000 tokens = $0.129
|
||||
|
||||
**Strategies**:
|
||||
- Generate fewer iterations
|
||||
- Use text modality first to validate concept
|
||||
- Batch similar requests
|
||||
- Cache prompts for consistent style
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Safety Filter Blocking
|
||||
|
||||
```python
|
||||
try:
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash-image',
|
||||
contents=prompt
|
||||
)
|
||||
except Exception as e:
|
||||
# Check block reason
|
||||
if hasattr(e, 'prompt_feedback'):
|
||||
print(f"Blocked: {e.prompt_feedback.block_reason}")
|
||||
# Modify prompt and retry
|
||||
```
|
||||
|
||||
### Token Limit Exceeded
|
||||
|
||||
```python
|
||||
# Keep prompts concise
|
||||
if len(prompt) > 1000:
|
||||
# Truncate or simplify
|
||||
prompt = prompt[:1000]
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum 3 input images for composition
|
||||
- Text rendering limited (25 chars max)
|
||||
- No video or animation generation
|
||||
- Regional restrictions (child images in EEA, CH, UK)
|
||||
- Optimal language support: English, Spanish (Mexico), Japanese, Mandarin, Hindi
|
||||
- No real-time generation
|
||||
- Cannot perfectly replicate specific people or copyrighted characters
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### aspect_ratio Parameter Error
|
||||
|
||||
**Error**: `Extra inputs are not permitted [type=extra_forbidden, input_value='1:1', input_type=str]`
|
||||
|
||||
**Cause**: The `aspect_ratio` parameter must be nested inside an `image_config` object, not passed directly to `GenerateContentConfig`.
|
||||
|
||||
**Incorrect Usage**:
|
||||
```python
|
||||
# ❌ This will fail
|
||||
config = types.GenerateContentConfig(
|
||||
response_modalities=['image'],
|
||||
aspect_ratio='16:9' # Wrong - not a direct parameter
|
||||
)
|
||||
```
|
||||
|
||||
**Correct Usage**:
|
||||
```python
|
||||
# ✅ Correct implementation
|
||||
config = types.GenerateContentConfig(
|
||||
response_modalities=['Image'], # Note: Capital 'I'
|
||||
image_config=types.ImageConfig(
|
||||
aspect_ratio='16:9'
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Response Modality Case Sensitivity
|
||||
|
||||
The `response_modalities` parameter expects capital case values:
|
||||
- ✅ Correct: `['Image']`, `['Text']`, `['Image', 'Text']`
|
||||
- ❌ Wrong: `['image']`, `['text']`
|
||||
502
skills/ai-multimodal/references/video-analysis.md
Normal file
502
skills/ai-multimodal/references/video-analysis.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# Video Analysis Reference
|
||||
|
||||
Comprehensive guide for video understanding, temporal analysis, and YouTube processing using Gemini API.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Video Summarization**: Create concise summaries
|
||||
- **Question Answering**: Answer specific questions about content
|
||||
- **Transcription**: Audio transcription with visual descriptions
|
||||
- **Timestamp References**: Query specific moments (MM:SS format)
|
||||
- **Video Clipping**: Process specific segments
|
||||
- **Scene Detection**: Identify scene changes and transitions
|
||||
- **Multiple Videos**: Compare up to 10 videos (2.5+)
|
||||
- **YouTube Support**: Analyze YouTube videos directly
|
||||
- **Custom Frame Rate**: Adjust FPS sampling
|
||||
|
||||
## Supported Formats
|
||||
|
||||
- MP4, MPEG, MOV, AVI, FLV, MPG, WebM, WMV, 3GPP
|
||||
|
||||
## Model Selection
|
||||
|
||||
### Gemini 2.5 Series
|
||||
- **gemini-2.5-pro**: Best quality, 1M-2M context
|
||||
- **gemini-2.5-flash**: Balanced, 1M-2M context
|
||||
- **gemini-2.5-flash-preview-09-2025**: Preview features, 1M context
|
||||
|
||||
### Gemini 2.0 Series
|
||||
- **gemini-2.0-flash**: Fast processing
|
||||
- **gemini-2.0-flash-lite**: Lightweight option
|
||||
|
||||
### Context Windows
|
||||
- **2M token models**: ~2 hours (default) or ~6 hours (low-res)
|
||||
- **1M token models**: ~1 hour (default) or ~3 hours (low-res)
|
||||
|
||||
## Basic Video Analysis
|
||||
|
||||
### Local Video
|
||||
|
||||
```python
|
||||
from google import genai
|
||||
import os
|
||||
|
||||
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
|
||||
|
||||
# Upload video (File API for >20MB)
|
||||
myfile = client.files.upload(file='video.mp4')
|
||||
|
||||
# Wait for processing
|
||||
import time
|
||||
while myfile.state.name == 'PROCESSING':
|
||||
time.sleep(1)
|
||||
myfile = client.files.get(name=myfile.name)
|
||||
|
||||
if myfile.state.name == 'FAILED':
|
||||
raise ValueError('Video processing failed')
|
||||
|
||||
# Analyze
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Summarize this video in 3 key points', myfile]
|
||||
)
|
||||
print(response.text)
|
||||
```
|
||||
|
||||
### YouTube Video
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Summarize the main topics discussed',
|
||||
types.Part.from_uri(
|
||||
uri='https://www.youtube.com/watch?v=VIDEO_ID',
|
||||
mime_type='video/mp4'
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Inline Video (<20MB)
|
||||
|
||||
```python
|
||||
with open('short-clip.mp4', 'rb') as f:
|
||||
video_bytes = f.read()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'What happens in this video?',
|
||||
types.Part.from_bytes(data=video_bytes, mime_type='video/mp4')
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Video Clipping
|
||||
|
||||
```python
|
||||
# Analyze specific time range
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Summarize this segment',
|
||||
types.Part.from_video_metadata(
|
||||
file_uri=myfile.uri,
|
||||
start_offset='40s',
|
||||
end_offset='80s'
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Custom Frame Rate
|
||||
|
||||
```python
|
||||
# Lower FPS for static content (saves tokens)
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Analyze this presentation',
|
||||
types.Part.from_video_metadata(
|
||||
file_uri=myfile.uri,
|
||||
fps=0.5 # Sample every 2 seconds
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Higher FPS for fast-moving content
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Analyze rapid movements in this sports video',
|
||||
types.Part.from_video_metadata(
|
||||
file_uri=myfile.uri,
|
||||
fps=5 # Sample 5 times per second
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Multiple Videos (2.5+)
|
||||
|
||||
```python
|
||||
video1 = client.files.upload(file='demo1.mp4')
|
||||
video2 = client.files.upload(file='demo2.mp4')
|
||||
|
||||
# Wait for processing
|
||||
for video in [video1, video2]:
|
||||
while video.state.name == 'PROCESSING':
|
||||
time.sleep(1)
|
||||
video = client.files.get(name=video.name)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-pro',
|
||||
contents=[
|
||||
'Compare these two product demos. Which explains features better?',
|
||||
video1,
|
||||
video2
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Temporal Understanding
|
||||
|
||||
### Timestamp-Based Questions
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'What happens at 01:15 and how does it relate to 02:30?',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Timeline Creation
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Create a timeline with timestamps:
|
||||
- Key events
|
||||
- Scene changes
|
||||
- Important moments
|
||||
Format: MM:SS - Description
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Scene Detection
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Identify all scene changes with timestamps and describe each scene',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Transcription
|
||||
|
||||
### Basic Transcription
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Transcribe the audio from this video',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### With Visual Descriptions
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Transcribe with visual context:
|
||||
- Audio transcription
|
||||
- Visual descriptions of important moments
|
||||
- Timestamps for salient events
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Speaker Identification
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Transcribe with speaker labels and timestamps',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Video Summarization
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Summarize this video:
|
||||
1. Main topic and purpose
|
||||
2. Key points with timestamps
|
||||
3. Conclusion or call-to-action
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Educational Content
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Create educational materials:
|
||||
1. List key concepts taught
|
||||
2. Create 5 quiz questions with answers
|
||||
3. Provide timestamp for each concept
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Action Detection
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'List all actions performed in this tutorial with timestamps',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Content Moderation
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Review video content:
|
||||
1. Identify any problematic content
|
||||
2. Note timestamps of concerns
|
||||
3. Provide content rating recommendation
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Interview Analysis
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Analyze interview:
|
||||
1. Questions asked (timestamps)
|
||||
2. Key responses
|
||||
3. Candidate body language and demeanor
|
||||
4. Overall assessment
|
||||
''',
|
||||
myfile
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 6. Sports Analysis
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Analyze sports video:
|
||||
1. Key plays with timestamps
|
||||
2. Player movements and positioning
|
||||
3. Game strategy observations
|
||||
''',
|
||||
types.Part.from_video_metadata(
|
||||
file_uri=myfile.uri,
|
||||
fps=5 # Higher FPS for fast action
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## YouTube Specific Features
|
||||
|
||||
### Public Video Requirements
|
||||
|
||||
- Video must be public (not private or unlisted)
|
||||
- No age-restricted content
|
||||
- Valid video ID required
|
||||
|
||||
### Usage Example
|
||||
|
||||
```python
|
||||
# YouTube URL
|
||||
youtube_uri = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Create chapter markers with timestamps',
|
||||
types.Part.from_uri(uri=youtube_uri, mime_type='video/mp4')
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Rate Limits
|
||||
|
||||
- **Free tier**: 8 hours of YouTube video per day
|
||||
- **Paid tier**: No length-based limits
|
||||
- Public videos only
|
||||
|
||||
## Token Calculation
|
||||
|
||||
Video tokens depend on resolution and FPS:
|
||||
|
||||
**Default resolution** (~300 tokens/second):
|
||||
- 1 minute = 18,000 tokens
|
||||
- 10 minutes = 180,000 tokens
|
||||
- 1 hour = 1,080,000 tokens
|
||||
|
||||
**Low resolution** (~100 tokens/second):
|
||||
- 1 minute = 6,000 tokens
|
||||
- 10 minutes = 60,000 tokens
|
||||
- 1 hour = 360,000 tokens
|
||||
|
||||
**Context windows**:
|
||||
- 2M tokens ≈ 2 hours (default) or 6 hours (low-res)
|
||||
- 1M tokens ≈ 1 hour (default) or 3 hours (low-res)
|
||||
|
||||
## Best Practices
|
||||
|
||||
### File Management
|
||||
|
||||
1. Use File API for videos >20MB (most videos)
|
||||
2. Wait for ACTIVE state before analysis
|
||||
3. Files auto-delete after 48 hours
|
||||
4. Clean up manually:
|
||||
```python
|
||||
client.files.delete(name=myfile.name)
|
||||
```
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
**Reduce token usage**:
|
||||
- Process specific segments using start/end offsets
|
||||
- Use lower FPS for static content
|
||||
- Use low-resolution mode for long videos
|
||||
- Split very long videos into chunks
|
||||
|
||||
**Improve accuracy**:
|
||||
- Provide context in prompts
|
||||
- Use higher FPS for fast-moving content
|
||||
- Use Pro model for complex analysis
|
||||
- Be specific about what to extract
|
||||
|
||||
### Prompt Engineering
|
||||
|
||||
**Effective prompts**:
|
||||
- "Summarize key points with timestamps in MM:SS format"
|
||||
- "Identify all scene changes and describe each scene"
|
||||
- "Extract action items mentioned with timestamps"
|
||||
- "Compare these two videos on: X, Y, Z criteria"
|
||||
|
||||
**Structured output**:
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
class VideoEvent(BaseModel):
|
||||
timestamp: str # MM:SS format
|
||||
description: str
|
||||
category: str
|
||||
|
||||
class VideoAnalysis(BaseModel):
|
||||
summary: str
|
||||
events: List[VideoEvent]
|
||||
duration: str
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Analyze this video', myfile],
|
||||
config=genai.types.GenerateContentConfig(
|
||||
response_mime_type='application/json',
|
||||
response_schema=VideoAnalysis
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def upload_and_process_video(file_path, max_wait=300):
|
||||
"""Upload video and wait for processing"""
|
||||
myfile = client.files.upload(file=file_path)
|
||||
|
||||
elapsed = 0
|
||||
while myfile.state.name == 'PROCESSING' and elapsed < max_wait:
|
||||
time.sleep(5)
|
||||
myfile = client.files.get(name=myfile.name)
|
||||
elapsed += 5
|
||||
|
||||
if myfile.state.name == 'FAILED':
|
||||
raise ValueError(f'Video processing failed: {myfile.state.name}')
|
||||
|
||||
if myfile.state.name == 'PROCESSING':
|
||||
raise TimeoutError(f'Processing timeout after {max_wait}s')
|
||||
|
||||
return myfile
|
||||
```
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
**Token costs** (Gemini 2.5 Flash at $1/1M):
|
||||
- 1 minute video (default): 18,000 tokens = $0.018
|
||||
- 10 minute video: 180,000 tokens = $0.18
|
||||
- 1 hour video: 1,080,000 tokens = $1.08
|
||||
|
||||
**Strategies**:
|
||||
- Use video clipping for specific segments
|
||||
- Lower FPS for static content
|
||||
- Use low-resolution mode for long videos
|
||||
- Batch related queries on same video
|
||||
- Use context caching for repeated queries
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum 6 hours (low-res) or 2 hours (default)
|
||||
- YouTube videos must be public
|
||||
- No live streaming analysis
|
||||
- Files expire after 48 hours
|
||||
- Processing time varies by video length
|
||||
- No real-time processing
|
||||
- Limited to 10 videos per request (2.5+)
|
||||
483
skills/ai-multimodal/references/vision-understanding.md
Normal file
483
skills/ai-multimodal/references/vision-understanding.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Vision Understanding Reference
|
||||
|
||||
Comprehensive guide for image analysis, object detection, and visual understanding using Gemini API.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Captioning**: Generate descriptive text for images
|
||||
- **Classification**: Categorize and identify content
|
||||
- **Visual Q&A**: Answer questions about images
|
||||
- **Object Detection**: Locate objects with bounding boxes (2.0+)
|
||||
- **Segmentation**: Create pixel-level masks (2.5+)
|
||||
- **Multi-image**: Compare up to 3,600 images
|
||||
- **OCR**: Extract text from images
|
||||
- **Document Understanding**: Process PDFs with vision
|
||||
|
||||
## Supported Formats
|
||||
|
||||
- **Images**: PNG, JPEG, WEBP, HEIC, HEIF
|
||||
- **Documents**: PDF (up to 1,000 pages)
|
||||
- **Size Limits**:
|
||||
- Inline: 20MB max total request
|
||||
- File API: 2GB per file
|
||||
- Max images: 3,600 per request
|
||||
|
||||
## Model Selection
|
||||
|
||||
### Gemini 2.5 Series
|
||||
- **gemini-2.5-pro**: Best quality, segmentation + detection
|
||||
- **gemini-2.5-flash**: Fast, efficient, all features
|
||||
- **gemini-2.5-flash-lite**: Lightweight, all features
|
||||
|
||||
### Gemini 2.0 Series
|
||||
- **gemini-2.0-flash**: Object detection support
|
||||
- **gemini-2.0-flash-lite**: Lightweight detection
|
||||
|
||||
### Feature Requirements
|
||||
- **Segmentation**: Requires 2.5+ models
|
||||
- **Object Detection**: Requires 2.0+ models
|
||||
- **Multi-image**: All models (up to 3,600 images)
|
||||
|
||||
## Basic Image Analysis
|
||||
|
||||
### Image Captioning
|
||||
|
||||
```python
|
||||
from google import genai
|
||||
import os
|
||||
|
||||
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
|
||||
|
||||
# Local file
|
||||
with open('image.jpg', 'rb') as f:
|
||||
img_bytes = f.read()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Describe this image in detail',
|
||||
genai.types.Part.from_bytes(data=img_bytes, mime_type='image/jpeg')
|
||||
]
|
||||
)
|
||||
print(response.text)
|
||||
```
|
||||
|
||||
### Image Classification
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Classify this image. Provide category and confidence level.',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Visual Question Answering
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'How many people are in this image and what are they doing?',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Object Detection (2.0+)
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.0-flash',
|
||||
contents=[
|
||||
'Detect all objects in this image and provide bounding boxes',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
|
||||
# Returns bounding box coordinates: [ymin, xmin, ymax, xmax]
|
||||
# Normalized to [0, 1000] range
|
||||
```
|
||||
|
||||
### Segmentation (2.5+)
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Create a segmentation mask for all people in this image',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
|
||||
# Returns pixel-level masks for requested objects
|
||||
```
|
||||
|
||||
### Multi-Image Comparison
|
||||
|
||||
```python
|
||||
import PIL.Image
|
||||
|
||||
img1 = PIL.Image.open('photo1.jpg')
|
||||
img2 = PIL.Image.open('photo2.jpg')
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Compare these two images. What are the differences?',
|
||||
img1,
|
||||
img2
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### OCR and Text Extraction
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Extract all visible text from this image',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Input Methods
|
||||
|
||||
### Inline Data (<20MB)
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
|
||||
# From file
|
||||
with open('image.jpg', 'rb') as f:
|
||||
img_bytes = f.read()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Analyze this image',
|
||||
types.Part.from_bytes(data=img_bytes, mime_type='image/jpeg')
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### PIL Image
|
||||
|
||||
```python
|
||||
import PIL.Image
|
||||
|
||||
img = PIL.Image.open('photo.jpg')
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['What is in this image?', img]
|
||||
)
|
||||
```
|
||||
|
||||
### File API (>20MB or Reuse)
|
||||
|
||||
```python
|
||||
# Upload once
|
||||
myfile = client.files.upload(file='large-image.jpg')
|
||||
|
||||
# Use multiple times
|
||||
response1 = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Describe this image', myfile]
|
||||
)
|
||||
|
||||
response2 = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['What colors dominate this image?', myfile]
|
||||
)
|
||||
```
|
||||
|
||||
### URL (Public Images)
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Analyze this image',
|
||||
types.Part.from_uri(
|
||||
uri='https://example.com/image.jpg',
|
||||
mime_type='image/jpeg'
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Token Calculation
|
||||
|
||||
Images consume tokens based on size:
|
||||
|
||||
**Small images** (≤384px both dimensions): 258 tokens
|
||||
|
||||
**Large images**: Tiled into 768×768 chunks, 258 tokens each
|
||||
|
||||
**Formula**:
|
||||
```
|
||||
crop_unit = floor(min(width, height) / 1.5)
|
||||
tiles = (width / crop_unit) × (height / crop_unit)
|
||||
total_tokens = tiles × 258
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- 256×256: 258 tokens (small)
|
||||
- 512×512: 258 tokens (small)
|
||||
- 960×540: 6 tiles = 1,548 tokens
|
||||
- 1920×1080: 6 tiles = 1,548 tokens
|
||||
- 3840×2160 (4K): 24 tiles = 6,192 tokens
|
||||
|
||||
## Structured Output
|
||||
|
||||
### JSON Schema Output
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
class ObjectDetection(BaseModel):
|
||||
object_name: str
|
||||
confidence: float
|
||||
bounding_box: List[int] # [ymin, xmin, ymax, xmax]
|
||||
|
||||
class ImageAnalysis(BaseModel):
|
||||
description: str
|
||||
objects: List[ObjectDetection]
|
||||
scene_type: str
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Analyze this image', img_part],
|
||||
config=genai.types.GenerateContentConfig(
|
||||
response_mime_type='application/json',
|
||||
response_schema=ImageAnalysis
|
||||
)
|
||||
)
|
||||
|
||||
result = ImageAnalysis.model_validate_json(response.text)
|
||||
```
|
||||
|
||||
## Multi-Image Analysis
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```python
|
||||
images = [
|
||||
PIL.Image.open(f'image{i}.jpg')
|
||||
for i in range(10)
|
||||
]
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=['Analyze these images and find common themes'] + images
|
||||
)
|
||||
```
|
||||
|
||||
### Image Comparison
|
||||
|
||||
```python
|
||||
before = PIL.Image.open('before.jpg')
|
||||
after = PIL.Image.open('after.jpg')
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Compare before and after. List all visible changes.',
|
||||
before,
|
||||
after
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Visual Search
|
||||
|
||||
```python
|
||||
reference = PIL.Image.open('target.jpg')
|
||||
candidates = [PIL.Image.open(f'option{i}.jpg') for i in range(5)]
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Find which candidate images contain objects similar to the reference',
|
||||
reference
|
||||
] + candidates
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Image Quality
|
||||
|
||||
1. **Resolution**: Use clear, non-blurry images
|
||||
2. **Rotation**: Verify correct orientation
|
||||
3. **Lighting**: Ensure good contrast and lighting
|
||||
4. **Size optimization**: Balance quality vs token cost
|
||||
5. **Format**: JPEG for photos, PNG for graphics
|
||||
|
||||
### Prompt Engineering
|
||||
|
||||
**Specific instructions**:
|
||||
- "Identify all vehicles with their colors and positions"
|
||||
- "Count people wearing blue shirts"
|
||||
- "Extract text from the sign in the top-left corner"
|
||||
|
||||
**Output format**:
|
||||
- "Return results as JSON with fields: category, count, description"
|
||||
- "Format as markdown table"
|
||||
- "List findings as numbered items"
|
||||
|
||||
**Few-shot examples**:
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Example: For an image of a cat on a sofa, respond: "Object: cat, Location: sofa"',
|
||||
'Now analyze this image:',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### File Management
|
||||
|
||||
1. Use File API for images >20MB
|
||||
2. Use File API for repeated queries (saves tokens)
|
||||
3. Files auto-delete after 48 hours
|
||||
4. Clean up manually:
|
||||
```python
|
||||
client.files.delete(name=myfile.name)
|
||||
```
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
**Token-efficient strategies**:
|
||||
- Resize large images before upload
|
||||
- Use File API for repeated queries
|
||||
- Batch multiple images when related
|
||||
- Use appropriate model (Flash vs Pro)
|
||||
|
||||
**Token costs** (Gemini 2.5 Flash at $1/1M):
|
||||
- Small image (258 tokens): $0.000258
|
||||
- HD image (1,548 tokens): $0.001548
|
||||
- 4K image (6,192 tokens): $0.006192
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Product Analysis
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Analyze this product image:
|
||||
1. Identify the product
|
||||
2. List visible features
|
||||
3. Assess condition
|
||||
4. Estimate value range
|
||||
''',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Screenshot Analysis
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Extract all text and UI elements from this screenshot',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Medical Imaging (Informational Only)
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-pro',
|
||||
contents=[
|
||||
'Describe visible features in this medical image. Note: This is for informational purposes only.',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Chart/Graph Reading
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'Extract data from this chart and format as JSON',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Scene Understanding
|
||||
|
||||
```python
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
'''Analyze this scene:
|
||||
1. Location type
|
||||
2. Time of day
|
||||
3. Weather conditions
|
||||
4. Activities happening
|
||||
5. Mood/atmosphere
|
||||
''',
|
||||
img_part
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def analyze_image_with_retry(image_path, prompt, max_retries=3):
|
||||
"""Analyze image with exponential backoff retry"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
with open(image_path, 'rb') as f:
|
||||
img_bytes = f.read()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents=[
|
||||
prompt,
|
||||
genai.types.Part.from_bytes(
|
||||
data=img_bytes,
|
||||
mime_type='image/jpeg'
|
||||
)
|
||||
]
|
||||
)
|
||||
return response.text
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
wait_time = 2 ** attempt
|
||||
print(f"Retry {attempt + 1} after {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum 3,600 images per request
|
||||
- OCR accuracy varies with text quality
|
||||
- Object detection requires 2.0+ models
|
||||
- Segmentation requires 2.5+ models
|
||||
- No video frame extraction (use video API)
|
||||
- Regional restrictions on child images (EEA, CH, UK)
|
||||
395
skills/ai-multimodal/scripts/document_converter.py
Normal file
395
skills/ai-multimodal/scripts/document_converter.py
Normal file
@@ -0,0 +1,395 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convert documents to Markdown using Gemini API.
|
||||
|
||||
Supports all document types:
|
||||
- PDF documents (native vision processing)
|
||||
- Images (JPEG, PNG, WEBP, HEIC)
|
||||
- Office documents (DOCX, XLSX, PPTX)
|
||||
- HTML, TXT, and other text formats
|
||||
|
||||
Features:
|
||||
- Converts to clean markdown format
|
||||
- Preserves structure, tables, and formatting
|
||||
- Extracts text from images and scanned documents
|
||||
- Batch conversion support
|
||||
- Saves to docs/assets/document-extraction.md by default
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed")
|
||||
print("Install with: pip install google-genai")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ImportError:
|
||||
load_dotenv = None
|
||||
|
||||
|
||||
def find_api_key() -> Optional[str]:
|
||||
"""Find Gemini API key using correct priority order.
|
||||
|
||||
Priority order (highest to lowest):
|
||||
1. process.env (runtime environment variables)
|
||||
2. .claude/skills/ai-multimodal/.env (skill-specific config)
|
||||
3. .claude/skills/.env (shared skills config)
|
||||
4. .claude/.env (Claude global config)
|
||||
"""
|
||||
# Priority 1: Already in process.env (highest)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Load .env files if dotenv available
|
||||
if load_dotenv:
|
||||
# Determine base paths
|
||||
script_dir = Path(__file__).parent
|
||||
skill_dir = script_dir.parent # .claude/skills/ai-multimodal
|
||||
skills_dir = skill_dir.parent # .claude/skills
|
||||
claude_dir = skills_dir.parent # .claude
|
||||
|
||||
# Priority 2: Skill-specific .env
|
||||
env_file = skill_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Priority 3: Shared skills .env
|
||||
env_file = skills_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Priority 4: Claude global .env
|
||||
env_file = claude_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_project_root() -> Path:
|
||||
"""Find project root directory."""
|
||||
script_dir = Path(__file__).parent
|
||||
|
||||
# Look for .git or .claude directory
|
||||
for parent in [script_dir] + list(script_dir.parents):
|
||||
if (parent / '.git').exists() or (parent / '.claude').exists():
|
||||
return parent
|
||||
|
||||
return script_dir
|
||||
|
||||
|
||||
def get_mime_type(file_path: str) -> str:
|
||||
"""Determine MIME type from file extension."""
|
||||
ext = Path(file_path).suffix.lower()
|
||||
|
||||
mime_types = {
|
||||
# Documents
|
||||
'.pdf': 'application/pdf',
|
||||
'.txt': 'text/plain',
|
||||
'.html': 'text/html',
|
||||
'.htm': 'text/html',
|
||||
'.md': 'text/markdown',
|
||||
'.csv': 'text/csv',
|
||||
# Images
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.webp': 'image/webp',
|
||||
'.heic': 'image/heic',
|
||||
'.heif': 'image/heif',
|
||||
# Office (need to be uploaded as binary)
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
}
|
||||
|
||||
return mime_types.get(ext, 'application/octet-stream')
|
||||
|
||||
|
||||
def upload_file(client: genai.Client, file_path: str, verbose: bool = False) -> Any:
|
||||
"""Upload file to Gemini File API."""
|
||||
if verbose:
|
||||
print(f"Uploading {file_path}...")
|
||||
|
||||
myfile = client.files.upload(file=file_path)
|
||||
|
||||
# Wait for processing if needed
|
||||
max_wait = 300 # 5 minutes
|
||||
elapsed = 0
|
||||
while myfile.state.name == 'PROCESSING' and elapsed < max_wait:
|
||||
time.sleep(2)
|
||||
myfile = client.files.get(name=myfile.name)
|
||||
elapsed += 2
|
||||
if verbose and elapsed % 10 == 0:
|
||||
print(f" Processing... {elapsed}s")
|
||||
|
||||
if myfile.state.name == 'FAILED':
|
||||
raise ValueError(f"File processing failed: {file_path}")
|
||||
|
||||
if myfile.state.name == 'PROCESSING':
|
||||
raise TimeoutError(f"Processing timeout after {max_wait}s: {file_path}")
|
||||
|
||||
if verbose:
|
||||
print(f" Uploaded: {myfile.name}")
|
||||
|
||||
return myfile
|
||||
|
||||
|
||||
def convert_to_markdown(
|
||||
client: genai.Client,
|
||||
file_path: str,
|
||||
model: str = 'gemini-2.5-flash',
|
||||
custom_prompt: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
max_retries: int = 3
|
||||
) -> Dict[str, Any]:
|
||||
"""Convert a document to markdown using Gemini."""
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
file_path_obj = Path(file_path)
|
||||
file_size = file_path_obj.stat().st_size
|
||||
use_file_api = file_size > 20 * 1024 * 1024 # >20MB
|
||||
|
||||
# Default prompt for markdown conversion
|
||||
if custom_prompt:
|
||||
prompt = custom_prompt
|
||||
else:
|
||||
prompt = """Convert this document to clean, well-formatted Markdown.
|
||||
|
||||
Requirements:
|
||||
- Preserve all content, structure, and formatting
|
||||
- Convert tables to markdown table format
|
||||
- Maintain heading hierarchy (# ## ### etc)
|
||||
- Preserve lists, code blocks, and quotes
|
||||
- Extract text from images if present
|
||||
- Keep formatting consistent and readable
|
||||
|
||||
Output only the markdown content without any preamble or explanation."""
|
||||
|
||||
# Upload or inline the file
|
||||
if use_file_api:
|
||||
myfile = upload_file(client, str(file_path), verbose)
|
||||
content = [prompt, myfile]
|
||||
else:
|
||||
with open(file_path, 'rb') as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
mime_type = get_mime_type(str(file_path))
|
||||
content = [
|
||||
prompt,
|
||||
types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
||||
]
|
||||
|
||||
# Generate markdown
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=content
|
||||
)
|
||||
|
||||
markdown_content = response.text if hasattr(response, 'text') else ''
|
||||
|
||||
return {
|
||||
'file': str(file_path),
|
||||
'status': 'success',
|
||||
'markdown': markdown_content
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
return {
|
||||
'file': str(file_path),
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'markdown': None
|
||||
}
|
||||
|
||||
wait_time = 2 ** attempt
|
||||
if verbose:
|
||||
print(f" Retry {attempt + 1} after {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
|
||||
|
||||
def batch_convert(
|
||||
files: List[str],
|
||||
output_file: Optional[str] = None,
|
||||
auto_name: bool = False,
|
||||
model: str = 'gemini-2.5-flash',
|
||||
custom_prompt: Optional[str] = None,
|
||||
verbose: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Batch convert multiple files to markdown."""
|
||||
|
||||
api_key = find_api_key()
|
||||
if not api_key:
|
||||
print("Error: GEMINI_API_KEY not found")
|
||||
print("Set via: export GEMINI_API_KEY='your-key'")
|
||||
print("Or create .env file with: GEMINI_API_KEY=your-key")
|
||||
sys.exit(1)
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
results = []
|
||||
|
||||
# Determine output path
|
||||
if not output_file:
|
||||
project_root = find_project_root()
|
||||
output_dir = project_root / 'docs' / 'assets'
|
||||
|
||||
if auto_name and len(files) == 1:
|
||||
# Auto-generate meaningful filename from input
|
||||
input_path = Path(files[0])
|
||||
base_name = input_path.stem
|
||||
output_file = str(output_dir / f"{base_name}-extraction.md")
|
||||
else:
|
||||
output_file = str(output_dir / 'document-extraction.md')
|
||||
|
||||
output_path = Path(output_file)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Process each file
|
||||
for i, file_path in enumerate(files, 1):
|
||||
if verbose:
|
||||
print(f"\n[{i}/{len(files)}] Converting: {file_path}")
|
||||
|
||||
result = convert_to_markdown(
|
||||
client=client,
|
||||
file_path=file_path,
|
||||
model=model,
|
||||
custom_prompt=custom_prompt,
|
||||
verbose=verbose
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
if verbose:
|
||||
status = result.get('status', 'unknown')
|
||||
print(f" Status: {status}")
|
||||
|
||||
# Save combined markdown
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write("# Document Extraction Results\n\n")
|
||||
f.write(f"Converted {len(files)} document(s) to markdown.\n\n")
|
||||
f.write("---\n\n")
|
||||
|
||||
for result in results:
|
||||
f.write(f"## {Path(result['file']).name}\n\n")
|
||||
|
||||
if result['status'] == 'success' and result.get('markdown'):
|
||||
f.write(result['markdown'])
|
||||
f.write("\n\n")
|
||||
elif result['status'] == 'success':
|
||||
f.write("**Note**: Conversion succeeded but no content was returned.\n\n")
|
||||
else:
|
||||
f.write(f"**Error**: {result.get('error', 'Unknown error')}\n\n")
|
||||
|
||||
f.write("---\n\n")
|
||||
|
||||
if verbose or True: # Always show output location
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Converted: {len(results)} file(s)")
|
||||
print(f"Success: {sum(1 for r in results if r['status'] == 'success')}")
|
||||
print(f"Failed: {sum(1 for r in results if r['status'] == 'error')}")
|
||||
print(f"Output saved to: {output_path}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Convert documents to Markdown using Gemini API',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Convert single PDF to markdown (default name)
|
||||
%(prog)s --input document.pdf
|
||||
|
||||
# Auto-generate meaningful filename
|
||||
%(prog)s --input testpdf.pdf --auto-name
|
||||
# Output: docs/assets/testpdf-extraction.md
|
||||
|
||||
# Convert multiple files
|
||||
%(prog)s --input doc1.pdf doc2.docx image.png
|
||||
|
||||
# Specify custom output location
|
||||
%(prog)s --input document.pdf --output ./output.md
|
||||
|
||||
# Use custom prompt
|
||||
%(prog)s --input document.pdf --prompt "Extract only the tables as markdown"
|
||||
|
||||
# Batch convert directory
|
||||
%(prog)s --input ./documents/*.pdf --verbose
|
||||
|
||||
Supported formats:
|
||||
- PDF documents (up to 1,000 pages)
|
||||
- Images (JPEG, PNG, WEBP, HEIC)
|
||||
- Office documents (DOCX, XLSX, PPTX)
|
||||
- Text formats (TXT, HTML, Markdown, CSV)
|
||||
|
||||
Default output: <project-root>/docs/assets/document-extraction.md
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('--input', '-i', nargs='+', required=True,
|
||||
help='Input file(s) to convert')
|
||||
parser.add_argument('--output', '-o',
|
||||
help='Output markdown file (default: docs/assets/document-extraction.md)')
|
||||
parser.add_argument('--auto-name', '-a', action='store_true',
|
||||
help='Auto-generate meaningful output filename from input (e.g., document.pdf -> document-extraction.md)')
|
||||
parser.add_argument('--model', default='gemini-2.5-flash',
|
||||
help='Gemini model to use (default: gemini-2.5-flash)')
|
||||
parser.add_argument('--prompt', '-p',
|
||||
help='Custom prompt for conversion')
|
||||
parser.add_argument('--verbose', '-v', action='store_true',
|
||||
help='Verbose output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate input files
|
||||
files = []
|
||||
for file_pattern in args.input:
|
||||
file_path = Path(file_pattern)
|
||||
if file_path.exists() and file_path.is_file():
|
||||
files.append(str(file_path))
|
||||
else:
|
||||
# Try glob pattern
|
||||
import glob
|
||||
matched = glob.glob(file_pattern)
|
||||
files.extend([f for f in matched if Path(f).is_file()])
|
||||
|
||||
if not files:
|
||||
print("Error: No valid input files found")
|
||||
sys.exit(1)
|
||||
|
||||
# Convert files
|
||||
batch_convert(
|
||||
files=files,
|
||||
output_file=args.output,
|
||||
auto_name=args.auto_name,
|
||||
model=args.model,
|
||||
custom_prompt=args.prompt,
|
||||
verbose=args.verbose
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
480
skills/ai-multimodal/scripts/gemini_batch_process.py
Normal file
480
skills/ai-multimodal/scripts/gemini_batch_process.py
Normal file
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Batch process multiple media files using Gemini API.
|
||||
|
||||
Supports all Gemini modalities:
|
||||
- Audio: Transcription, analysis, summarization
|
||||
- Image: Captioning, detection, OCR, analysis
|
||||
- Video: Summarization, Q&A, scene detection
|
||||
- Document: PDF extraction, structured output
|
||||
- Generation: Image creation from text prompts
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
import csv
|
||||
import shutil
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed")
|
||||
print("Install with: pip install google-genai")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ImportError:
|
||||
load_dotenv = None
|
||||
|
||||
|
||||
def find_api_key() -> Optional[str]:
|
||||
"""Find Gemini API key using correct priority order.
|
||||
|
||||
Priority order (highest to lowest):
|
||||
1. process.env (runtime environment variables)
|
||||
2. .claude/skills/ai-multimodal/.env (skill-specific config)
|
||||
3. .claude/skills/.env (shared skills config)
|
||||
4. .claude/.env (Claude global config)
|
||||
"""
|
||||
# Priority 1: Already in process.env (highest)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Load .env files if dotenv available
|
||||
if load_dotenv:
|
||||
# Determine base paths
|
||||
script_dir = Path(__file__).parent
|
||||
skill_dir = script_dir.parent # .claude/skills/ai-multimodal
|
||||
skills_dir = skill_dir.parent # .claude/skills
|
||||
claude_dir = skills_dir.parent # .claude
|
||||
|
||||
# Priority 2: Skill-specific .env
|
||||
env_file = skill_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Priority 3: Shared skills .env
|
||||
env_file = skills_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Priority 4: Claude global .env
|
||||
env_file = claude_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_mime_type(file_path: str) -> str:
|
||||
"""Determine MIME type from file extension."""
|
||||
ext = Path(file_path).suffix.lower()
|
||||
|
||||
mime_types = {
|
||||
# Audio
|
||||
'.mp3': 'audio/mp3',
|
||||
'.wav': 'audio/wav',
|
||||
'.aac': 'audio/aac',
|
||||
'.flac': 'audio/flac',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.aiff': 'audio/aiff',
|
||||
# Image
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.webp': 'image/webp',
|
||||
'.heic': 'image/heic',
|
||||
'.heif': 'image/heif',
|
||||
# Video
|
||||
'.mp4': 'video/mp4',
|
||||
'.mpeg': 'video/mpeg',
|
||||
'.mov': 'video/quicktime',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.flv': 'video/x-flv',
|
||||
'.mpg': 'video/mpeg',
|
||||
'.webm': 'video/webm',
|
||||
'.wmv': 'video/x-ms-wmv',
|
||||
'.3gpp': 'video/3gpp',
|
||||
# Document
|
||||
'.pdf': 'application/pdf',
|
||||
'.txt': 'text/plain',
|
||||
'.html': 'text/html',
|
||||
'.md': 'text/markdown',
|
||||
}
|
||||
|
||||
return mime_types.get(ext, 'application/octet-stream')
|
||||
|
||||
|
||||
def upload_file(client: genai.Client, file_path: str, verbose: bool = False) -> Any:
|
||||
"""Upload file to Gemini File API."""
|
||||
if verbose:
|
||||
print(f"Uploading {file_path}...")
|
||||
|
||||
myfile = client.files.upload(file=file_path)
|
||||
|
||||
# Wait for processing (video/audio files need processing)
|
||||
mime_type = get_mime_type(file_path)
|
||||
if mime_type.startswith('video/') or mime_type.startswith('audio/'):
|
||||
max_wait = 300 # 5 minutes
|
||||
elapsed = 0
|
||||
while myfile.state.name == 'PROCESSING' and elapsed < max_wait:
|
||||
time.sleep(2)
|
||||
myfile = client.files.get(name=myfile.name)
|
||||
elapsed += 2
|
||||
if verbose and elapsed % 10 == 0:
|
||||
print(f" Processing... {elapsed}s")
|
||||
|
||||
if myfile.state.name == 'FAILED':
|
||||
raise ValueError(f"File processing failed: {file_path}")
|
||||
|
||||
if myfile.state.name == 'PROCESSING':
|
||||
raise TimeoutError(f"Processing timeout after {max_wait}s: {file_path}")
|
||||
|
||||
if verbose:
|
||||
print(f" Uploaded: {myfile.name}")
|
||||
|
||||
return myfile
|
||||
|
||||
|
||||
def process_file(
|
||||
client: genai.Client,
|
||||
file_path: Optional[str],
|
||||
prompt: str,
|
||||
model: str,
|
||||
task: str,
|
||||
format_output: str,
|
||||
aspect_ratio: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
max_retries: int = 3
|
||||
) -> Dict[str, Any]:
|
||||
"""Process a single file with retry logic."""
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# For generation tasks without input files
|
||||
if task == 'generate' and not file_path:
|
||||
content = [prompt]
|
||||
else:
|
||||
# Process input file
|
||||
file_path = Path(file_path)
|
||||
# Determine if we need File API
|
||||
file_size = file_path.stat().st_size
|
||||
use_file_api = file_size > 20 * 1024 * 1024 # >20MB
|
||||
|
||||
if use_file_api:
|
||||
# Upload to File API
|
||||
myfile = upload_file(client, str(file_path), verbose)
|
||||
content = [prompt, myfile]
|
||||
else:
|
||||
# Inline data
|
||||
with open(file_path, 'rb') as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
mime_type = get_mime_type(str(file_path))
|
||||
content = [
|
||||
prompt,
|
||||
types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
||||
]
|
||||
|
||||
# Configure request
|
||||
config_args = {}
|
||||
if task == 'generate':
|
||||
config_args['response_modalities'] = ['Image'] # Capital I per API spec
|
||||
if aspect_ratio:
|
||||
# Nest aspect_ratio in image_config per API spec
|
||||
config_args['image_config'] = types.ImageConfig(
|
||||
aspect_ratio=aspect_ratio
|
||||
)
|
||||
|
||||
if format_output == 'json':
|
||||
config_args['response_mime_type'] = 'application/json'
|
||||
|
||||
config = types.GenerateContentConfig(**config_args) if config_args else None
|
||||
|
||||
# Generate content
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=content,
|
||||
config=config
|
||||
)
|
||||
|
||||
# Extract response
|
||||
result = {
|
||||
'file': str(file_path) if file_path else 'generated',
|
||||
'status': 'success',
|
||||
'response': response.text if hasattr(response, 'text') else None
|
||||
}
|
||||
|
||||
# Handle image output
|
||||
if task == 'generate' and hasattr(response, 'candidates'):
|
||||
for i, part in enumerate(response.candidates[0].content.parts):
|
||||
if part.inline_data:
|
||||
# Determine output directory - use project root docs/assets
|
||||
if file_path:
|
||||
output_dir = Path(file_path).parent
|
||||
base_name = Path(file_path).stem
|
||||
else:
|
||||
# Find project root (look for .git or .claude directory)
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir
|
||||
for parent in [script_dir] + list(script_dir.parents):
|
||||
if (parent / '.git').exists() or (parent / '.claude').exists():
|
||||
project_root = parent
|
||||
break
|
||||
|
||||
output_dir = project_root / 'docs' / 'assets'
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
base_name = "generated"
|
||||
|
||||
output_file = output_dir / f"{base_name}_generated_{i}.png"
|
||||
with open(output_file, 'wb') as f:
|
||||
f.write(part.inline_data.data)
|
||||
result['generated_image'] = str(output_file)
|
||||
if verbose:
|
||||
print(f" Saved image to: {output_file}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
return {
|
||||
'file': str(file_path) if file_path else 'generated',
|
||||
'status': 'error',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
wait_time = 2 ** attempt
|
||||
if verbose:
|
||||
print(f" Retry {attempt + 1} after {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
|
||||
|
||||
def batch_process(
|
||||
files: List[str],
|
||||
prompt: str,
|
||||
model: str,
|
||||
task: str,
|
||||
format_output: str,
|
||||
aspect_ratio: Optional[str] = None,
|
||||
output_file: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Batch process multiple files."""
|
||||
api_key = find_api_key()
|
||||
if not api_key:
|
||||
print("Error: GEMINI_API_KEY not found")
|
||||
print("Set via: export GEMINI_API_KEY='your-key'")
|
||||
print("Or create .env file with: GEMINI_API_KEY=your-key")
|
||||
sys.exit(1)
|
||||
|
||||
if dry_run:
|
||||
print("DRY RUN MODE - No API calls will be made")
|
||||
print(f"Files to process: {len(files)}")
|
||||
print(f"Model: {model}")
|
||||
print(f"Task: {task}")
|
||||
print(f"Prompt: {prompt}")
|
||||
return []
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
results = []
|
||||
|
||||
# For generation tasks without input files, process once
|
||||
if task == 'generate' and not files:
|
||||
if verbose:
|
||||
print(f"\nGenerating image from prompt...")
|
||||
|
||||
result = process_file(
|
||||
client=client,
|
||||
file_path=None,
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
task=task,
|
||||
format_output=format_output,
|
||||
aspect_ratio=aspect_ratio,
|
||||
verbose=verbose
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
if verbose:
|
||||
status = result.get('status', 'unknown')
|
||||
print(f" Status: {status}")
|
||||
else:
|
||||
# Process input files
|
||||
for i, file_path in enumerate(files, 1):
|
||||
if verbose:
|
||||
print(f"\n[{i}/{len(files)}] Processing: {file_path}")
|
||||
|
||||
result = process_file(
|
||||
client=client,
|
||||
file_path=file_path,
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
task=task,
|
||||
format_output=format_output,
|
||||
aspect_ratio=aspect_ratio,
|
||||
verbose=verbose
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
if verbose:
|
||||
status = result.get('status', 'unknown')
|
||||
print(f" Status: {status}")
|
||||
|
||||
# Save results
|
||||
if output_file:
|
||||
save_results(results, output_file, format_output)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def save_results(results: List[Dict[str, Any]], output_file: str, format_output: str):
|
||||
"""Save results to file."""
|
||||
output_path = Path(output_file)
|
||||
|
||||
# Special handling for image generation - if output has image extension, copy the generated image
|
||||
image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp'}
|
||||
if output_path.suffix.lower() in image_extensions and len(results) == 1:
|
||||
generated_image = results[0].get('generated_image')
|
||||
if generated_image:
|
||||
# Copy the generated image to the specified output location
|
||||
shutil.copy2(generated_image, output_path)
|
||||
return
|
||||
else:
|
||||
# Don't write text reports to image files - save error as .txt instead
|
||||
output_path = output_path.with_suffix('.error.txt')
|
||||
print(f"Warning: Generation failed, saving error report to: {output_path}")
|
||||
|
||||
if format_output == 'json':
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
elif format_output == 'csv':
|
||||
with open(output_path, 'w', newline='') as f:
|
||||
fieldnames = ['file', 'status', 'response', 'error']
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for result in results:
|
||||
writer.writerow({
|
||||
'file': result.get('file', ''),
|
||||
'status': result.get('status', ''),
|
||||
'response': result.get('response', ''),
|
||||
'error': result.get('error', '')
|
||||
})
|
||||
else: # markdown
|
||||
with open(output_path, 'w') as f:
|
||||
f.write("# Batch Processing Results\n\n")
|
||||
for i, result in enumerate(results, 1):
|
||||
f.write(f"## {i}. {result.get('file', 'Unknown')}\n\n")
|
||||
f.write(f"**Status**: {result.get('status', 'unknown')}\n\n")
|
||||
if result.get('response'):
|
||||
f.write(f"**Response**:\n\n{result['response']}\n\n")
|
||||
if result.get('error'):
|
||||
f.write(f"**Error**: {result['error']}\n\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Batch process media files with Gemini API',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Transcribe multiple audio files
|
||||
%(prog)s --files *.mp3 --task transcribe --model gemini-2.5-flash
|
||||
|
||||
# Analyze images
|
||||
%(prog)s --files *.jpg --task analyze --prompt "Describe this image" \\
|
||||
--model gemini-2.5-flash
|
||||
|
||||
# Process PDFs to JSON
|
||||
%(prog)s --files *.pdf --task extract --prompt "Extract data as JSON" \\
|
||||
--format json --output results.json
|
||||
|
||||
# Generate images
|
||||
%(prog)s --task generate --prompt "A mountain landscape" \\
|
||||
--model gemini-2.5-flash-image --aspect-ratio 16:9
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('--files', nargs='*', help='Input files to process')
|
||||
parser.add_argument('--task', required=True,
|
||||
choices=['transcribe', 'analyze', 'extract', 'generate'],
|
||||
help='Task to perform')
|
||||
parser.add_argument('--prompt', help='Prompt for analysis/generation')
|
||||
parser.add_argument('--model', default='gemini-2.5-flash',
|
||||
help='Gemini model to use (default: gemini-2.5-flash)')
|
||||
parser.add_argument('--format', dest='format_output', default='text',
|
||||
choices=['text', 'json', 'csv', 'markdown'],
|
||||
help='Output format (default: text)')
|
||||
parser.add_argument('--aspect-ratio', choices=['1:1', '16:9', '9:16', '4:3', '3:4'],
|
||||
help='Aspect ratio for image generation')
|
||||
parser.add_argument('--output', help='Output file for results')
|
||||
parser.add_argument('--verbose', '-v', action='store_true',
|
||||
help='Verbose output')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='Show what would be done without making API calls')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if args.task != 'generate' and not args.files:
|
||||
parser.error("--files required for non-generation tasks")
|
||||
|
||||
if args.task == 'generate' and not args.prompt:
|
||||
parser.error("--prompt required for generation task")
|
||||
|
||||
if args.task != 'generate' and not args.prompt:
|
||||
# Set default prompts
|
||||
if args.task == 'transcribe':
|
||||
args.prompt = 'Generate a transcript with timestamps'
|
||||
elif args.task == 'analyze':
|
||||
args.prompt = 'Analyze this content'
|
||||
elif args.task == 'extract':
|
||||
args.prompt = 'Extract key information'
|
||||
|
||||
# Process files
|
||||
files = args.files or []
|
||||
results = batch_process(
|
||||
files=files,
|
||||
prompt=args.prompt,
|
||||
model=args.model,
|
||||
task=args.task,
|
||||
format_output=args.format_output,
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
output_file=args.output,
|
||||
verbose=args.verbose,
|
||||
dry_run=args.dry_run
|
||||
)
|
||||
|
||||
# Print summary
|
||||
if not args.dry_run and results:
|
||||
success = sum(1 for r in results if r.get('status') == 'success')
|
||||
failed = len(results) - success
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Processed: {len(results)} files")
|
||||
print(f"Success: {success}")
|
||||
print(f"Failed: {failed}")
|
||||
if args.output:
|
||||
print(f"Results saved to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
506
skills/ai-multimodal/scripts/media_optimizer.py
Normal file
506
skills/ai-multimodal/scripts/media_optimizer.py
Normal file
@@ -0,0 +1,506 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Optimize media files for Gemini API processing.
|
||||
|
||||
Features:
|
||||
- Compress videos/audio for size limits
|
||||
- Resize images appropriately
|
||||
- Split long videos into chunks
|
||||
- Format conversion
|
||||
- Quality vs size optimization
|
||||
- Validation before upload
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ImportError:
|
||||
load_dotenv = None
|
||||
|
||||
|
||||
def load_env_files():
|
||||
"""Load .env files in correct priority order.
|
||||
|
||||
Priority order (highest to lowest):
|
||||
1. process.env (runtime environment variables)
|
||||
2. .claude/skills/ai-multimodal/.env (skill-specific config)
|
||||
3. .claude/skills/.env (shared skills config)
|
||||
4. .claude/.env (Claude global config)
|
||||
"""
|
||||
if not load_dotenv:
|
||||
return
|
||||
|
||||
# Determine base paths
|
||||
script_dir = Path(__file__).parent
|
||||
skill_dir = script_dir.parent # .claude/skills/ai-multimodal
|
||||
skills_dir = skill_dir.parent # .claude/skills
|
||||
claude_dir = skills_dir.parent # .claude
|
||||
|
||||
# Priority 2: Skill-specific .env
|
||||
env_file = skill_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
|
||||
# Priority 3: Shared skills .env
|
||||
env_file = skills_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
|
||||
# Priority 4: Claude global .env
|
||||
env_file = claude_dir / '.env'
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
|
||||
|
||||
# Load environment variables at module level
|
||||
load_env_files()
|
||||
|
||||
|
||||
def check_ffmpeg() -> bool:
|
||||
"""Check if ffmpeg is installed."""
|
||||
try:
|
||||
subprocess.run(['ffmpeg', '-version'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, Exception):
|
||||
return False
|
||||
|
||||
|
||||
def get_media_info(file_path: str) -> Dict[str, Any]:
|
||||
"""Get media file information using ffprobe."""
|
||||
if not check_ffmpeg():
|
||||
return {}
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe',
|
||||
'-v', 'quiet',
|
||||
'-print_format', 'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
file_path
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
info = {
|
||||
'size': int(data['format'].get('size', 0)),
|
||||
'duration': float(data['format'].get('duration', 0)),
|
||||
'bit_rate': int(data['format'].get('bit_rate', 0)),
|
||||
}
|
||||
|
||||
# Get video/audio specific info
|
||||
for stream in data.get('streams', []):
|
||||
if stream['codec_type'] == 'video':
|
||||
info['width'] = stream.get('width', 0)
|
||||
info['height'] = stream.get('height', 0)
|
||||
info['fps'] = eval(stream.get('r_frame_rate', '0/1'))
|
||||
elif stream['codec_type'] == 'audio':
|
||||
info['sample_rate'] = int(stream.get('sample_rate', 0))
|
||||
info['channels'] = stream.get('channels', 0)
|
||||
|
||||
return info
|
||||
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError, Exception):
|
||||
return {}
|
||||
|
||||
|
||||
def optimize_video(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
target_size_mb: Optional[int] = None,
|
||||
max_duration: Optional[int] = None,
|
||||
quality: int = 23,
|
||||
resolution: Optional[str] = None,
|
||||
verbose: bool = False
|
||||
) -> bool:
|
||||
"""Optimize video file for Gemini API."""
|
||||
if not check_ffmpeg():
|
||||
print("Error: ffmpeg not installed")
|
||||
print("Install: apt-get install ffmpeg (Linux) or brew install ffmpeg (Mac)")
|
||||
return False
|
||||
|
||||
info = get_media_info(input_path)
|
||||
if not info:
|
||||
print(f"Error: Could not read media info from {input_path}")
|
||||
return False
|
||||
|
||||
if verbose:
|
||||
print(f"Input: {Path(input_path).name}")
|
||||
print(f" Size: {info['size'] / (1024*1024):.2f} MB")
|
||||
print(f" Duration: {info['duration']:.2f}s")
|
||||
if 'width' in info:
|
||||
print(f" Resolution: {info['width']}x{info['height']}")
|
||||
print(f" Bit rate: {info['bit_rate'] / 1000:.0f} kbps")
|
||||
|
||||
# Build ffmpeg command
|
||||
cmd = ['ffmpeg', '-i', input_path, '-y']
|
||||
|
||||
# Video codec
|
||||
cmd.extend(['-c:v', 'libx264', '-crf', str(quality)])
|
||||
|
||||
# Resolution
|
||||
if resolution:
|
||||
cmd.extend(['-vf', f'scale={resolution}'])
|
||||
elif 'width' in info and info['width'] > 1920:
|
||||
cmd.extend(['-vf', 'scale=1920:-2']) # Max 1080p
|
||||
|
||||
# Audio codec
|
||||
cmd.extend(['-c:a', 'aac', '-b:a', '128k', '-ac', '2'])
|
||||
|
||||
# Duration limit
|
||||
if max_duration and info['duration'] > max_duration:
|
||||
cmd.extend(['-t', str(max_duration)])
|
||||
|
||||
# Target size (rough estimate using bitrate)
|
||||
if target_size_mb:
|
||||
target_bits = target_size_mb * 8 * 1024 * 1024
|
||||
duration = min(info['duration'], max_duration) if max_duration else info['duration']
|
||||
target_bitrate = int(target_bits / duration)
|
||||
# Reserve some for audio (128kbps)
|
||||
video_bitrate = max(target_bitrate - 128000, 500000)
|
||||
cmd.extend(['-b:v', str(video_bitrate)])
|
||||
|
||||
cmd.append(output_path)
|
||||
|
||||
if verbose:
|
||||
print(f"\nOptimizing...")
|
||||
print(f" Command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=not verbose)
|
||||
|
||||
# Check output
|
||||
output_info = get_media_info(output_path)
|
||||
if output_info and verbose:
|
||||
print(f"\nOutput: {Path(output_path).name}")
|
||||
print(f" Size: {output_info['size'] / (1024*1024):.2f} MB")
|
||||
print(f" Duration: {output_info['duration']:.2f}s")
|
||||
if 'width' in output_info:
|
||||
print(f" Resolution: {output_info['width']}x{output_info['height']}")
|
||||
compression = (1 - output_info['size'] / info['size']) * 100
|
||||
print(f" Compression: {compression:.1f}%")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error optimizing video: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def optimize_audio(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
target_size_mb: Optional[int] = None,
|
||||
bitrate: str = '64k',
|
||||
sample_rate: int = 16000,
|
||||
verbose: bool = False
|
||||
) -> bool:
|
||||
"""Optimize audio file for Gemini API."""
|
||||
if not check_ffmpeg():
|
||||
print("Error: ffmpeg not installed")
|
||||
return False
|
||||
|
||||
info = get_media_info(input_path)
|
||||
if not info:
|
||||
print(f"Error: Could not read media info from {input_path}")
|
||||
return False
|
||||
|
||||
if verbose:
|
||||
print(f"Input: {Path(input_path).name}")
|
||||
print(f" Size: {info['size'] / (1024*1024):.2f} MB")
|
||||
print(f" Duration: {info['duration']:.2f}s")
|
||||
|
||||
# Build command
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path, '-y',
|
||||
'-c:a', 'aac',
|
||||
'-b:a', bitrate,
|
||||
'-ar', str(sample_rate),
|
||||
'-ac', '1', # Mono (Gemini uses mono anyway)
|
||||
output_path
|
||||
]
|
||||
|
||||
if verbose:
|
||||
print(f"\nOptimizing...")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=not verbose)
|
||||
|
||||
output_info = get_media_info(output_path)
|
||||
if output_info and verbose:
|
||||
print(f"\nOutput: {Path(output_path).name}")
|
||||
print(f" Size: {output_info['size'] / (1024*1024):.2f} MB")
|
||||
compression = (1 - output_info['size'] / info['size']) * 100
|
||||
print(f" Compression: {compression:.1f}%")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error optimizing audio: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def optimize_image(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
max_width: int = 1920,
|
||||
quality: int = 85,
|
||||
verbose: bool = False
|
||||
) -> bool:
|
||||
"""Optimize image file for Gemini API."""
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("Error: Pillow not installed")
|
||||
print("Install with: pip install pillow")
|
||||
return False
|
||||
|
||||
try:
|
||||
img = Image.open(input_path)
|
||||
|
||||
if verbose:
|
||||
print(f"Input: {Path(input_path).name}")
|
||||
print(f" Size: {Path(input_path).stat().st_size / 1024:.2f} KB")
|
||||
print(f" Resolution: {img.width}x{img.height}")
|
||||
|
||||
# Resize if needed
|
||||
if img.width > max_width:
|
||||
ratio = max_width / img.width
|
||||
new_height = int(img.height * ratio)
|
||||
img = img.resize((max_width, new_height), Image.Resampling.LANCZOS)
|
||||
if verbose:
|
||||
print(f" Resized to: {img.width}x{img.height}")
|
||||
|
||||
# Convert RGBA to RGB if saving as JPEG
|
||||
if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
|
||||
if img.mode == 'RGBA':
|
||||
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
|
||||
rgb_img.paste(img, mask=img.split()[3])
|
||||
img = rgb_img
|
||||
|
||||
# Save
|
||||
img.save(output_path, quality=quality, optimize=True)
|
||||
|
||||
if verbose:
|
||||
print(f"\nOutput: {Path(output_path).name}")
|
||||
print(f" Size: {Path(output_path).stat().st_size / 1024:.2f} KB")
|
||||
compression = (1 - Path(output_path).stat().st_size / Path(input_path).stat().st_size) * 100
|
||||
print(f" Compression: {compression:.1f}%")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error optimizing image: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def split_video(
|
||||
input_path: str,
|
||||
output_dir: str,
|
||||
chunk_duration: int = 3600,
|
||||
verbose: bool = False
|
||||
) -> List[str]:
|
||||
"""Split long video into chunks."""
|
||||
if not check_ffmpeg():
|
||||
print("Error: ffmpeg not installed")
|
||||
return []
|
||||
|
||||
info = get_media_info(input_path)
|
||||
if not info:
|
||||
return []
|
||||
|
||||
total_duration = info['duration']
|
||||
num_chunks = int(total_duration / chunk_duration) + 1
|
||||
|
||||
if num_chunks == 1:
|
||||
if verbose:
|
||||
print("Video is short enough, no splitting needed")
|
||||
return [input_path]
|
||||
|
||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
output_files = []
|
||||
|
||||
for i in range(num_chunks):
|
||||
start_time = i * chunk_duration
|
||||
output_file = Path(output_dir) / f"{Path(input_path).stem}_chunk_{i+1}.mp4"
|
||||
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path, '-y',
|
||||
'-ss', str(start_time),
|
||||
'-t', str(chunk_duration),
|
||||
'-c', 'copy',
|
||||
str(output_file)
|
||||
]
|
||||
|
||||
if verbose:
|
||||
print(f"Creating chunk {i+1}/{num_chunks}...")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=not verbose)
|
||||
output_files.append(str(output_file))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error creating chunk {i+1}: {e}")
|
||||
|
||||
return output_files
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Optimize media files for Gemini API',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Optimize video to 100MB
|
||||
%(prog)s --input video.mp4 --output optimized.mp4 --target-size 100
|
||||
|
||||
# Optimize audio
|
||||
%(prog)s --input audio.mp3 --output optimized.m4a --bitrate 64k
|
||||
|
||||
# Resize image
|
||||
%(prog)s --input image.jpg --output resized.jpg --max-width 1920
|
||||
|
||||
# Split long video
|
||||
%(prog)s --input long-video.mp4 --split --chunk-duration 3600 --output-dir ./chunks
|
||||
|
||||
# Batch optimize directory
|
||||
%(prog)s --input-dir ./videos --output-dir ./optimized --quality 85
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('--input', help='Input file')
|
||||
parser.add_argument('--output', help='Output file')
|
||||
parser.add_argument('--input-dir', help='Input directory for batch processing')
|
||||
parser.add_argument('--output-dir', help='Output directory for batch processing')
|
||||
parser.add_argument('--target-size', type=int, help='Target size in MB')
|
||||
parser.add_argument('--quality', type=int, default=85,
|
||||
help='Quality (video: 0-51 CRF, image: 1-100) (default: 85)')
|
||||
parser.add_argument('--max-width', type=int, default=1920,
|
||||
help='Max image width (default: 1920)')
|
||||
parser.add_argument('--bitrate', default='64k',
|
||||
help='Audio bitrate (default: 64k)')
|
||||
parser.add_argument('--resolution', help='Video resolution (e.g., 1920x1080)')
|
||||
parser.add_argument('--split', action='store_true', help='Split long video into chunks')
|
||||
parser.add_argument('--chunk-duration', type=int, default=3600,
|
||||
help='Chunk duration in seconds (default: 3600 = 1 hour)')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if not args.input and not args.input_dir:
|
||||
parser.error("Either --input or --input-dir required")
|
||||
|
||||
# Single file processing
|
||||
if args.input:
|
||||
input_path = Path(args.input)
|
||||
if not input_path.exists():
|
||||
print(f"Error: Input file not found: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.split:
|
||||
output_dir = args.output_dir or './chunks'
|
||||
chunks = split_video(str(input_path), output_dir, args.chunk_duration, args.verbose)
|
||||
print(f"\nCreated {len(chunks)} chunks in {output_dir}")
|
||||
sys.exit(0)
|
||||
|
||||
if not args.output:
|
||||
parser.error("--output required for single file processing")
|
||||
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Determine file type
|
||||
ext = input_path.suffix.lower()
|
||||
|
||||
if ext in ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv']:
|
||||
success = optimize_video(
|
||||
str(input_path),
|
||||
str(output_path),
|
||||
target_size_mb=args.target_size,
|
||||
quality=args.quality,
|
||||
resolution=args.resolution,
|
||||
verbose=args.verbose
|
||||
)
|
||||
elif ext in ['.mp3', '.wav', '.m4a', '.flac', '.aac']:
|
||||
success = optimize_audio(
|
||||
str(input_path),
|
||||
str(output_path),
|
||||
target_size_mb=args.target_size,
|
||||
bitrate=args.bitrate,
|
||||
verbose=args.verbose
|
||||
)
|
||||
elif ext in ['.jpg', '.jpeg', '.png', '.webp']:
|
||||
success = optimize_image(
|
||||
str(input_path),
|
||||
str(output_path),
|
||||
max_width=args.max_width,
|
||||
quality=args.quality,
|
||||
verbose=args.verbose
|
||||
)
|
||||
else:
|
||||
print(f"Error: Unsupported file type: {ext}")
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
# Batch processing
|
||||
if args.input_dir:
|
||||
if not args.output_dir:
|
||||
parser.error("--output-dir required for batch processing")
|
||||
|
||||
input_dir = Path(args.input_dir)
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Find all media files
|
||||
patterns = ['*.mp4', '*.mov', '*.avi', '*.mkv', '*.webm',
|
||||
'*.mp3', '*.wav', '*.m4a', '*.flac',
|
||||
'*.jpg', '*.jpeg', '*.png', '*.webp']
|
||||
|
||||
files = []
|
||||
for pattern in patterns:
|
||||
files.extend(input_dir.glob(pattern))
|
||||
|
||||
if not files:
|
||||
print(f"No media files found in {input_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Found {len(files)} files to process")
|
||||
|
||||
success_count = 0
|
||||
for input_file in files:
|
||||
output_file = output_dir / input_file.name
|
||||
|
||||
ext = input_file.suffix.lower()
|
||||
success = False
|
||||
|
||||
if ext in ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv']:
|
||||
success = optimize_video(str(input_file), str(output_file),
|
||||
quality=args.quality, verbose=args.verbose)
|
||||
elif ext in ['.mp3', '.wav', '.m4a', '.flac', '.aac']:
|
||||
success = optimize_audio(str(input_file), str(output_file),
|
||||
bitrate=args.bitrate, verbose=args.verbose)
|
||||
elif ext in ['.jpg', '.jpeg', '.png', '.webp']:
|
||||
success = optimize_image(str(input_file), str(output_file),
|
||||
max_width=args.max_width, quality=args.quality,
|
||||
verbose=args.verbose)
|
||||
|
||||
if success:
|
||||
success_count += 1
|
||||
|
||||
print(f"\nProcessed: {success_count}/{len(files)} files")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
26
skills/ai-multimodal/scripts/requirements.txt
Normal file
26
skills/ai-multimodal/scripts/requirements.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
# AI Multimodal Skill Dependencies
|
||||
# Python 3.10+ required
|
||||
|
||||
# Google Gemini API
|
||||
google-genai>=0.1.0
|
||||
|
||||
# PDF processing
|
||||
pypdf>=4.0.0
|
||||
|
||||
# Document conversion
|
||||
python-docx>=1.0.0
|
||||
docx2pdf>=0.1.8 # Windows only, optional on Linux/macOS
|
||||
|
||||
# Markdown processing
|
||||
markdown>=3.5.0
|
||||
|
||||
# Image processing
|
||||
Pillow>=10.0.0
|
||||
|
||||
# Environment variable management
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Testing dependencies (dev)
|
||||
pytest>=8.0.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-mock>=3.12.0
|
||||
20
skills/ai-multimodal/scripts/tests/requirements.txt
Normal file
20
skills/ai-multimodal/scripts/tests/requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
# Core dependencies
|
||||
google-genai>=0.2.0
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Image processing
|
||||
pillow>=10.0.0
|
||||
|
||||
# PDF processing
|
||||
pypdf>=3.0.0
|
||||
|
||||
# Document conversion
|
||||
markdown>=3.5
|
||||
|
||||
# Testing
|
||||
pytest>=7.4.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-mock>=3.12.0
|
||||
|
||||
# Optional dependencies for full functionality
|
||||
# ffmpeg-python>=0.2.0 # For media optimization (requires ffmpeg installed)
|
||||
299
skills/ai-multimodal/scripts/tests/test_document_converter.py
Normal file
299
skills/ai-multimodal/scripts/tests/test_document_converter.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Tests for document_converter.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import document_converter as dc
|
||||
|
||||
|
||||
class TestEnvLoading:
|
||||
"""Test environment variable loading."""
|
||||
|
||||
@patch('document_converter.load_dotenv')
|
||||
@patch('pathlib.Path.exists')
|
||||
def test_load_env_files_success(self, mock_exists, mock_load_dotenv):
|
||||
"""Test successful .env file loading."""
|
||||
mock_exists.return_value = True
|
||||
dc.load_env_files()
|
||||
# Should be called for skill, skills, and claude dirs
|
||||
assert mock_load_dotenv.call_count >= 1
|
||||
|
||||
@patch('document_converter.load_dotenv', None)
|
||||
def test_load_env_files_no_dotenv(self):
|
||||
"""Test when dotenv is not available."""
|
||||
# Should not raise an error
|
||||
dc.load_env_files()
|
||||
|
||||
|
||||
class TestDependencyCheck:
|
||||
"""Test dependency checking."""
|
||||
|
||||
@patch('builtins.__import__')
|
||||
def test_check_all_dependencies_available(self, mock_import):
|
||||
"""Test when all dependencies are available."""
|
||||
mock_import.return_value = Mock()
|
||||
|
||||
deps = dc.check_dependencies()
|
||||
|
||||
assert 'pypdf' in deps
|
||||
assert 'markdown' in deps
|
||||
assert 'pillow' in deps
|
||||
|
||||
@patch('builtins.__import__')
|
||||
def test_check_dependencies_missing(self, mock_import):
|
||||
"""Test when dependencies are missing."""
|
||||
def import_side_effect(name, *args, **kwargs):
|
||||
if name == 'pypdf':
|
||||
raise ImportError()
|
||||
return Mock()
|
||||
|
||||
mock_import.side_effect = import_side_effect
|
||||
|
||||
# The function uses try/except, so we test the actual function
|
||||
with patch('document_converter.sys.modules', {}):
|
||||
# This is tricky to test due to import handling
|
||||
pass
|
||||
|
||||
|
||||
class TestPDFPageExtraction:
|
||||
"""Test PDF page extraction."""
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('pypdf.PdfWriter')
|
||||
@patch('builtins.open', create=True)
|
||||
def test_extract_single_page(self, mock_open, mock_writer_class, mock_reader_class):
|
||||
"""Test extracting a single page."""
|
||||
# Mock reader
|
||||
mock_reader = Mock()
|
||||
mock_page = Mock()
|
||||
mock_reader.pages = [Mock(), mock_page, Mock()]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
# Mock writer
|
||||
mock_writer = Mock()
|
||||
mock_writer.pages = [mock_page]
|
||||
mock_writer_class.return_value = mock_writer
|
||||
|
||||
result = dc.extract_pdf_pages(
|
||||
'input.pdf',
|
||||
'output.pdf',
|
||||
page_range='2',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_writer.add_page.assert_called_once_with(mock_page)
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('pypdf.PdfWriter')
|
||||
@patch('builtins.open', create=True)
|
||||
def test_extract_page_range(self, mock_open, mock_writer_class, mock_reader_class):
|
||||
"""Test extracting a range of pages."""
|
||||
mock_reader = Mock()
|
||||
mock_reader.pages = [Mock() for _ in range(10)]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
mock_writer = Mock()
|
||||
mock_writer.pages = []
|
||||
mock_writer_class.return_value = mock_writer
|
||||
|
||||
result = dc.extract_pdf_pages(
|
||||
'input.pdf',
|
||||
'output.pdf',
|
||||
page_range='2-5',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert mock_writer.add_page.call_count == 4 # Pages 2-5 (4 pages)
|
||||
|
||||
def test_extract_pages_no_pypdf(self):
|
||||
"""Test page extraction without pypdf."""
|
||||
with patch.dict('sys.modules', {'pypdf': None}):
|
||||
result = dc.extract_pdf_pages('input.pdf', 'output.pdf', '1-10')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestPDFOptimization:
|
||||
"""Test PDF optimization."""
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('pypdf.PdfWriter')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_pdf_success(self, mock_stat, mock_open, mock_writer_class, mock_reader_class):
|
||||
"""Test successful PDF optimization."""
|
||||
# Mock reader
|
||||
mock_reader = Mock()
|
||||
mock_page = Mock()
|
||||
mock_reader.pages = [mock_page, mock_page]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
# Mock writer
|
||||
mock_writer = Mock()
|
||||
mock_writer.pages = [mock_page, mock_page]
|
||||
mock_writer_class.return_value = mock_writer
|
||||
|
||||
# Mock file sizes
|
||||
mock_stat.return_value.st_size = 1024 * 1024
|
||||
|
||||
result = dc.optimize_pdf('input.pdf', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is True
|
||||
mock_page.compress_content_streams.assert_called()
|
||||
|
||||
def test_optimize_pdf_no_pypdf(self):
|
||||
"""Test PDF optimization without pypdf."""
|
||||
with patch.dict('sys.modules', {'pypdf': None}):
|
||||
result = dc.optimize_pdf('input.pdf', 'output.pdf')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestImageExtraction:
|
||||
"""Test image extraction from PDFs."""
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('PIL.Image')
|
||||
@patch('pathlib.Path.mkdir')
|
||||
@patch('builtins.open', create=True)
|
||||
def test_extract_images_success(self, mock_open, mock_mkdir, mock_image, mock_reader_class):
|
||||
"""Test successful image extraction."""
|
||||
# Mock PDF reader
|
||||
mock_reader = Mock()
|
||||
mock_page = MagicMock()
|
||||
|
||||
# Mock XObject with image
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.__getitem__.side_effect = lambda k: {
|
||||
'/Subtype': '/Image',
|
||||
'/Width': 100,
|
||||
'/Height': 100,
|
||||
'/Filter': '/DCTDecode'
|
||||
}[k]
|
||||
mock_obj.get_data.return_value = b'image_data'
|
||||
|
||||
mock_xobjects = MagicMock()
|
||||
mock_xobjects.__iter__.return_value = ['img1']
|
||||
mock_xobjects.__getitem__.return_value = mock_obj
|
||||
|
||||
mock_resources = MagicMock()
|
||||
mock_resources.get_object.return_value = mock_xobjects
|
||||
mock_page.__getitem__.side_effect = lambda k: {
|
||||
'/Resources': {'/XObject': mock_resources}
|
||||
}[k]
|
||||
|
||||
mock_reader.pages = [mock_page]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
result = dc.extract_images_from_pdf('input.pdf', './output', verbose=False)
|
||||
|
||||
assert len(result) > 0
|
||||
|
||||
def test_extract_images_no_dependencies(self):
|
||||
"""Test image extraction without required dependencies."""
|
||||
with patch.dict('sys.modules', {'pypdf': None}):
|
||||
result = dc.extract_images_from_pdf('input.pdf', './output')
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestMarkdownConversion:
|
||||
"""Test Markdown to PDF conversion."""
|
||||
|
||||
@patch('markdown.markdown')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('subprocess.run')
|
||||
@patch('pathlib.Path.unlink')
|
||||
def test_convert_markdown_success(self, mock_unlink, mock_run, mock_open, mock_markdown):
|
||||
"""Test successful Markdown to PDF conversion."""
|
||||
mock_markdown.return_value = '<h1>Test</h1>'
|
||||
|
||||
# Mock file reading and writing
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = '# Test'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
result = dc.convert_markdown_to_pdf('input.md', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('markdown.markdown')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('subprocess.run')
|
||||
def test_convert_markdown_no_wkhtmltopdf(self, mock_run, mock_open, mock_markdown):
|
||||
"""Test Markdown conversion without wkhtmltopdf."""
|
||||
mock_markdown.return_value = '<h1>Test</h1>'
|
||||
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = '# Test'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
|
||||
result = dc.convert_markdown_to_pdf('input.md', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_convert_markdown_no_markdown_lib(self):
|
||||
"""Test Markdown conversion without markdown library."""
|
||||
with patch.dict('sys.modules', {'markdown': None}):
|
||||
result = dc.convert_markdown_to_pdf('input.md', 'output.pdf')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestHTMLConversion:
|
||||
"""Test HTML to PDF conversion."""
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_convert_html_success(self, mock_run):
|
||||
"""Test successful HTML to PDF conversion."""
|
||||
result = dc.convert_html_to_pdf('input.html', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_convert_html_no_wkhtmltopdf(self, mock_run):
|
||||
"""Test HTML conversion without wkhtmltopdf."""
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
|
||||
result = dc.convert_html_to_pdf('input.html', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests."""
|
||||
|
||||
@patch('pathlib.Path.exists')
|
||||
def test_file_not_found(self, mock_exists):
|
||||
"""Test handling of non-existent input file."""
|
||||
mock_exists.return_value = False
|
||||
|
||||
# This would normally be tested via main() but we test the concept
|
||||
assert not Path('nonexistent.pdf').exists()
|
||||
|
||||
@patch('document_converter.check_dependencies')
|
||||
def test_check_dependencies_integration(self, mock_check):
|
||||
"""Test dependency checking integration."""
|
||||
mock_check.return_value = {
|
||||
'pypdf': True,
|
||||
'markdown': True,
|
||||
'pillow': True
|
||||
}
|
||||
|
||||
deps = dc.check_dependencies()
|
||||
|
||||
assert deps['pypdf'] is True
|
||||
assert deps['markdown'] is True
|
||||
assert deps['pillow'] is True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--cov=document_converter', '--cov-report=term-missing'])
|
||||
362
skills/ai-multimodal/scripts/tests/test_gemini_batch_process.py
Normal file
362
skills/ai-multimodal/scripts/tests/test_gemini_batch_process.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
Tests for gemini_batch_process.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import gemini_batch_process as gbp
|
||||
|
||||
|
||||
class TestAPIKeyFinder:
|
||||
"""Test API key detection."""
|
||||
|
||||
def test_find_api_key_from_env(self, monkeypatch):
|
||||
"""Test finding API key from environment variable."""
|
||||
monkeypatch.setenv('GEMINI_API_KEY', 'test_key_123')
|
||||
assert gbp.find_api_key() == 'test_key_123'
|
||||
|
||||
@patch('gemini_batch_process.load_dotenv')
|
||||
def test_find_api_key_not_found(self, mock_load_dotenv, monkeypatch):
|
||||
"""Test when API key is not found."""
|
||||
monkeypatch.delenv('GEMINI_API_KEY', raising=False)
|
||||
# Mock load_dotenv to not actually load any files
|
||||
mock_load_dotenv.return_value = None
|
||||
assert gbp.find_api_key() is None
|
||||
|
||||
|
||||
class TestMimeTypeDetection:
|
||||
"""Test MIME type detection."""
|
||||
|
||||
def test_audio_mime_types(self):
|
||||
"""Test audio file MIME types."""
|
||||
assert gbp.get_mime_type('test.mp3') == 'audio/mp3'
|
||||
assert gbp.get_mime_type('test.wav') == 'audio/wav'
|
||||
assert gbp.get_mime_type('test.aac') == 'audio/aac'
|
||||
assert gbp.get_mime_type('test.flac') == 'audio/flac'
|
||||
|
||||
def test_image_mime_types(self):
|
||||
"""Test image file MIME types."""
|
||||
assert gbp.get_mime_type('test.jpg') == 'image/jpeg'
|
||||
assert gbp.get_mime_type('test.jpeg') == 'image/jpeg'
|
||||
assert gbp.get_mime_type('test.png') == 'image/png'
|
||||
assert gbp.get_mime_type('test.webp') == 'image/webp'
|
||||
|
||||
def test_video_mime_types(self):
|
||||
"""Test video file MIME types."""
|
||||
assert gbp.get_mime_type('test.mp4') == 'video/mp4'
|
||||
assert gbp.get_mime_type('test.mov') == 'video/quicktime'
|
||||
assert gbp.get_mime_type('test.avi') == 'video/x-msvideo'
|
||||
|
||||
def test_document_mime_types(self):
|
||||
"""Test document file MIME types."""
|
||||
assert gbp.get_mime_type('test.pdf') == 'application/pdf'
|
||||
assert gbp.get_mime_type('test.txt') == 'text/plain'
|
||||
|
||||
def test_unknown_mime_type(self):
|
||||
"""Test unknown file extension."""
|
||||
assert gbp.get_mime_type('test.xyz') == 'application/octet-stream'
|
||||
|
||||
def test_case_insensitive(self):
|
||||
"""Test case-insensitive extension matching."""
|
||||
assert gbp.get_mime_type('TEST.MP3') == 'audio/mp3'
|
||||
assert gbp.get_mime_type('Test.JPG') == 'image/jpeg'
|
||||
|
||||
|
||||
class TestFileUpload:
|
||||
"""Test file upload functionality."""
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
def test_upload_file_success(self, mock_client_class):
|
||||
"""Test successful file upload."""
|
||||
# Mock client and file
|
||||
mock_client = Mock()
|
||||
mock_file = Mock()
|
||||
mock_file.state.name = 'ACTIVE'
|
||||
mock_file.name = 'test_file'
|
||||
mock_client.files.upload.return_value = mock_file
|
||||
|
||||
result = gbp.upload_file(mock_client, 'test.jpg', verbose=False)
|
||||
|
||||
assert result == mock_file
|
||||
mock_client.files.upload.assert_called_once_with(file='test.jpg')
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('gemini_batch_process.time.sleep')
|
||||
def test_upload_video_with_processing(self, mock_sleep, mock_client_class):
|
||||
"""Test video upload with processing wait."""
|
||||
mock_client = Mock()
|
||||
|
||||
# First call: PROCESSING, second call: ACTIVE
|
||||
mock_file_processing = Mock()
|
||||
mock_file_processing.state.name = 'PROCESSING'
|
||||
mock_file_processing.name = 'test_video'
|
||||
|
||||
mock_file_active = Mock()
|
||||
mock_file_active.state.name = 'ACTIVE'
|
||||
mock_file_active.name = 'test_video'
|
||||
|
||||
mock_client.files.upload.return_value = mock_file_processing
|
||||
mock_client.files.get.return_value = mock_file_active
|
||||
|
||||
result = gbp.upload_file(mock_client, 'test.mp4', verbose=False)
|
||||
|
||||
assert result.state.name == 'ACTIVE'
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
def test_upload_file_failed(self, mock_client_class):
|
||||
"""Test failed file upload."""
|
||||
mock_client = Mock()
|
||||
mock_file = Mock()
|
||||
mock_file.state.name = 'FAILED'
|
||||
mock_client.files.upload.return_value = mock_file
|
||||
mock_client.files.get.return_value = mock_file
|
||||
|
||||
with pytest.raises(ValueError, match="File processing failed"):
|
||||
gbp.upload_file(mock_client, 'test.mp4', verbose=False)
|
||||
|
||||
|
||||
class TestProcessFile:
|
||||
"""Test file processing functionality."""
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_process_small_file_inline(self, mock_stat, mock_open, mock_client_class):
|
||||
"""Test processing small file with inline data."""
|
||||
# Mock small file
|
||||
mock_stat.return_value.st_size = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
# Mock file content
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = b'test_data'
|
||||
|
||||
# Mock client and response
|
||||
mock_client = Mock()
|
||||
mock_response = Mock()
|
||||
mock_response.text = 'Test response'
|
||||
mock_client.models.generate_content.return_value = mock_response
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.jpg',
|
||||
prompt='Describe this image',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result['status'] == 'success'
|
||||
assert result['response'] == 'Test response'
|
||||
|
||||
@patch('gemini_batch_process.upload_file')
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_process_large_file_api(self, mock_stat, mock_client_class, mock_upload):
|
||||
"""Test processing large file with File API."""
|
||||
# Mock large file
|
||||
mock_stat.return_value.st_size = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
# Mock upload and response
|
||||
mock_file = Mock()
|
||||
mock_upload.return_value = mock_file
|
||||
|
||||
mock_client = Mock()
|
||||
mock_response = Mock()
|
||||
mock_response.text = 'Test response'
|
||||
mock_client.models.generate_content.return_value = mock_response
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.mp4',
|
||||
prompt='Summarize this video',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result['status'] == 'success'
|
||||
mock_upload.assert_called_once()
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_process_file_error_handling(self, mock_stat, mock_open, mock_client_class):
|
||||
"""Test error handling in file processing."""
|
||||
mock_stat.return_value.st_size = 1024
|
||||
|
||||
# Mock file read
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = b'test_data'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.models.generate_content.side_effect = Exception("API Error")
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.jpg',
|
||||
prompt='Test',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
max_retries=1
|
||||
)
|
||||
|
||||
assert result['status'] == 'error'
|
||||
assert 'API Error' in result['error']
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_image_generation_with_aspect_ratio(self, mock_stat, mock_open, mock_client_class):
|
||||
"""Test image generation with aspect ratio config."""
|
||||
mock_stat.return_value.st_size = 1024
|
||||
|
||||
# Mock file read
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = b'test'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
mock_client = Mock()
|
||||
mock_response = Mock()
|
||||
mock_response.candidates = [Mock()]
|
||||
mock_response.candidates[0].content.parts = [
|
||||
Mock(inline_data=Mock(data=b'fake_image_data'))
|
||||
]
|
||||
mock_client.models.generate_content.return_value = mock_response
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.txt',
|
||||
prompt='Generate mountain landscape',
|
||||
model='gemini-2.5-flash-image',
|
||||
task='generate',
|
||||
format_output='text',
|
||||
aspect_ratio='16:9',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Verify config was called with correct structure
|
||||
call_args = mock_client.models.generate_content.call_args
|
||||
config = call_args.kwargs.get('config')
|
||||
assert config is not None
|
||||
assert result['status'] == 'success'
|
||||
assert 'generated_image' in result
|
||||
|
||||
|
||||
class TestBatchProcessing:
|
||||
"""Test batch processing functionality."""
|
||||
|
||||
@patch('gemini_batch_process.find_api_key')
|
||||
@patch('gemini_batch_process.process_file')
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
def test_batch_process_success(self, mock_client_class, mock_process, mock_find_key):
|
||||
"""Test successful batch processing."""
|
||||
mock_find_key.return_value = 'test_key'
|
||||
mock_process.return_value = {'status': 'success', 'response': 'Test'}
|
||||
|
||||
results = gbp.batch_process(
|
||||
files=['test1.jpg', 'test2.jpg'],
|
||||
prompt='Analyze',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
dry_run=False
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert all(r['status'] == 'success' for r in results)
|
||||
|
||||
@patch('gemini_batch_process.find_api_key')
|
||||
def test_batch_process_no_api_key(self, mock_find_key):
|
||||
"""Test batch processing without API key."""
|
||||
mock_find_key.return_value = None
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
gbp.batch_process(
|
||||
files=['test.jpg'],
|
||||
prompt='Test',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
dry_run=False
|
||||
)
|
||||
|
||||
@patch('gemini_batch_process.find_api_key')
|
||||
def test_batch_process_dry_run(self, mock_find_key):
|
||||
"""Test dry run mode."""
|
||||
# API key not needed for dry run, but we mock it to avoid sys.exit
|
||||
mock_find_key.return_value = 'test_key'
|
||||
|
||||
results = gbp.batch_process(
|
||||
files=['test1.jpg', 'test2.jpg'],
|
||||
prompt='Test',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
dry_run=True
|
||||
)
|
||||
|
||||
assert results == []
|
||||
|
||||
|
||||
class TestResultsSaving:
|
||||
"""Test results saving functionality."""
|
||||
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('json.dump')
|
||||
def test_save_results_json(self, mock_json_dump, mock_open):
|
||||
"""Test saving results as JSON."""
|
||||
results = [
|
||||
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
|
||||
{'file': 'test2.jpg', 'status': 'success', 'response': 'Test2'}
|
||||
]
|
||||
|
||||
gbp.save_results(results, 'output.json', 'json')
|
||||
|
||||
mock_json_dump.assert_called_once()
|
||||
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('csv.DictWriter')
|
||||
def test_save_results_csv(self, mock_csv_writer, mock_open):
|
||||
"""Test saving results as CSV."""
|
||||
results = [
|
||||
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
|
||||
{'file': 'test2.jpg', 'status': 'success', 'response': 'Test2'}
|
||||
]
|
||||
|
||||
gbp.save_results(results, 'output.csv', 'csv')
|
||||
|
||||
# Verify CSV writer was used
|
||||
mock_csv_writer.assert_called_once()
|
||||
|
||||
@patch('builtins.open', create=True)
|
||||
def test_save_results_markdown(self, mock_open):
|
||||
"""Test saving results as Markdown."""
|
||||
mock_file = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
results = [
|
||||
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
|
||||
{'file': 'test2.jpg', 'status': 'error', 'error': 'Failed'}
|
||||
]
|
||||
|
||||
gbp.save_results(results, 'output.md', 'markdown')
|
||||
|
||||
# Verify write was called
|
||||
assert mock_file.write.call_count > 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--cov=gemini_batch_process', '--cov-report=term-missing'])
|
||||
373
skills/ai-multimodal/scripts/tests/test_media_optimizer.py
Normal file
373
skills/ai-multimodal/scripts/tests/test_media_optimizer.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
Tests for media_optimizer.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import json
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import media_optimizer as mo
|
||||
|
||||
|
||||
class TestEnvLoading:
|
||||
"""Test environment variable loading."""
|
||||
|
||||
@patch('media_optimizer.load_dotenv')
|
||||
@patch('pathlib.Path.exists')
|
||||
def test_load_env_files_success(self, mock_exists, mock_load_dotenv):
|
||||
"""Test successful .env file loading."""
|
||||
mock_exists.return_value = True
|
||||
mo.load_env_files()
|
||||
# Should be called for skill, skills, and claude dirs
|
||||
assert mock_load_dotenv.call_count >= 1
|
||||
|
||||
@patch('media_optimizer.load_dotenv', None)
|
||||
def test_load_env_files_no_dotenv(self):
|
||||
"""Test when dotenv is not available."""
|
||||
# Should not raise an error
|
||||
mo.load_env_files()
|
||||
|
||||
|
||||
class TestFFmpegCheck:
|
||||
"""Test ffmpeg availability checking."""
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_ffmpeg_installed(self, mock_run):
|
||||
"""Test when ffmpeg is installed."""
|
||||
mock_run.return_value = Mock()
|
||||
assert mo.check_ffmpeg() is True
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_ffmpeg_not_installed(self, mock_run):
|
||||
"""Test when ffmpeg is not installed."""
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
assert mo.check_ffmpeg() is False
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_ffmpeg_error(self, mock_run):
|
||||
"""Test ffmpeg command error."""
|
||||
mock_run.side_effect = Exception("Error")
|
||||
assert mo.check_ffmpeg() is False
|
||||
|
||||
|
||||
class TestMediaInfo:
|
||||
"""Test media information extraction."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('subprocess.run')
|
||||
def test_get_video_info(self, mock_run, mock_check):
|
||||
"""Test extracting video information."""
|
||||
mock_check.return_value = True
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps({
|
||||
'format': {
|
||||
'size': '10485760',
|
||||
'duration': '120.5',
|
||||
'bit_rate': '691200'
|
||||
},
|
||||
'streams': [
|
||||
{
|
||||
'codec_type': 'video',
|
||||
'width': 1920,
|
||||
'height': 1080,
|
||||
'r_frame_rate': '30/1'
|
||||
},
|
||||
{
|
||||
'codec_type': 'audio',
|
||||
'sample_rate': '48000',
|
||||
'channels': 2
|
||||
}
|
||||
]
|
||||
})
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
info = mo.get_media_info('test.mp4')
|
||||
|
||||
assert info['size'] == 10485760
|
||||
assert info['duration'] == 120.5
|
||||
assert info['width'] == 1920
|
||||
assert info['height'] == 1080
|
||||
assert info['sample_rate'] == 48000
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_get_media_info_no_ffmpeg(self, mock_check):
|
||||
"""Test when ffmpeg is not available."""
|
||||
mock_check.return_value = False
|
||||
info = mo.get_media_info('test.mp4')
|
||||
assert info == {}
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('subprocess.run')
|
||||
def test_get_media_info_error(self, mock_run, mock_check):
|
||||
"""Test error handling in media info extraction."""
|
||||
mock_check.return_value = True
|
||||
mock_run.side_effect = Exception("Error")
|
||||
|
||||
info = mo.get_media_info('test.mp4')
|
||||
assert info == {}
|
||||
|
||||
|
||||
class TestVideoOptimization:
|
||||
"""Test video optimization functionality."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_video_success(self, mock_run, mock_info, mock_check):
|
||||
"""Test successful video optimization."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
# Input info
|
||||
{
|
||||
'size': 50 * 1024 * 1024,
|
||||
'duration': 120.0,
|
||||
'bit_rate': 3500000,
|
||||
'width': 1920,
|
||||
'height': 1080
|
||||
},
|
||||
# Output info
|
||||
{
|
||||
'size': 25 * 1024 * 1024,
|
||||
'duration': 120.0,
|
||||
'width': 1920,
|
||||
'height': 1080
|
||||
}
|
||||
]
|
||||
|
||||
result = mo.optimize_video(
|
||||
'input.mp4',
|
||||
'output.mp4',
|
||||
quality=23,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_optimize_video_no_ffmpeg(self, mock_check):
|
||||
"""Test video optimization without ffmpeg."""
|
||||
mock_check.return_value = False
|
||||
|
||||
result = mo.optimize_video('input.mp4', 'output.mp4')
|
||||
assert result is False
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
def test_optimize_video_no_info(self, mock_info, mock_check):
|
||||
"""Test video optimization when info cannot be read."""
|
||||
mock_check.return_value = True
|
||||
mock_info.return_value = {}
|
||||
|
||||
result = mo.optimize_video('input.mp4', 'output.mp4')
|
||||
assert result is False
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_video_with_target_size(self, mock_run, mock_info, mock_check):
|
||||
"""Test video optimization with target size."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
{'size': 100 * 1024 * 1024, 'duration': 60.0, 'bit_rate': 3500000},
|
||||
{'size': 50 * 1024 * 1024, 'duration': 60.0}
|
||||
]
|
||||
|
||||
result = mo.optimize_video(
|
||||
'input.mp4',
|
||||
'output.mp4',
|
||||
target_size_mb=50,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_video_with_resolution(self, mock_run, mock_info, mock_check):
|
||||
"""Test video optimization with custom resolution."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
{'size': 50 * 1024 * 1024, 'duration': 120.0, 'bit_rate': 3500000},
|
||||
{'size': 25 * 1024 * 1024, 'duration': 120.0}
|
||||
]
|
||||
|
||||
result = mo.optimize_video(
|
||||
'input.mp4',
|
||||
'output.mp4',
|
||||
resolution='1280x720',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestAudioOptimization:
|
||||
"""Test audio optimization functionality."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_audio_success(self, mock_run, mock_info, mock_check):
|
||||
"""Test successful audio optimization."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
{'size': 10 * 1024 * 1024, 'duration': 300.0},
|
||||
{'size': 5 * 1024 * 1024, 'duration': 300.0}
|
||||
]
|
||||
|
||||
result = mo.optimize_audio(
|
||||
'input.mp3',
|
||||
'output.m4a',
|
||||
bitrate='64k',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_optimize_audio_no_ffmpeg(self, mock_check):
|
||||
"""Test audio optimization without ffmpeg."""
|
||||
mock_check.return_value = False
|
||||
|
||||
result = mo.optimize_audio('input.mp3', 'output.m4a')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestImageOptimization:
|
||||
"""Test image optimization functionality."""
|
||||
|
||||
@patch('PIL.Image.open')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_image_success(self, mock_stat, mock_image_open):
|
||||
"""Test successful image optimization."""
|
||||
# Mock image
|
||||
mock_resized = Mock()
|
||||
mock_resized.mode = 'RGB'
|
||||
|
||||
mock_img = Mock()
|
||||
mock_img.width = 3840
|
||||
mock_img.height = 2160
|
||||
mock_img.mode = 'RGB'
|
||||
mock_img.resize.return_value = mock_resized
|
||||
mock_image_open.return_value = mock_img
|
||||
|
||||
# Mock file sizes
|
||||
mock_stat.return_value.st_size = 5 * 1024 * 1024
|
||||
|
||||
result = mo.optimize_image(
|
||||
'input.jpg',
|
||||
'output.jpg',
|
||||
max_width=1920,
|
||||
quality=85,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
# Since image is resized, save is called on the resized image
|
||||
mock_resized.save.assert_called_once()
|
||||
|
||||
@patch('PIL.Image.open')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_image_resize(self, mock_stat, mock_image_open):
|
||||
"""Test image resizing during optimization."""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 3840
|
||||
mock_img.height = 2160
|
||||
mock_img.mode = 'RGB'
|
||||
mock_resized = Mock()
|
||||
mock_img.resize.return_value = mock_resized
|
||||
mock_image_open.return_value = mock_img
|
||||
|
||||
mock_stat.return_value.st_size = 5 * 1024 * 1024
|
||||
|
||||
mo.optimize_image('input.jpg', 'output.jpg', max_width=1920, verbose=False)
|
||||
|
||||
mock_img.resize.assert_called_once()
|
||||
|
||||
@patch('PIL.Image.open')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_image_rgba_to_jpg(self, mock_stat, mock_image_open):
|
||||
"""Test converting RGBA to RGB for JPEG."""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 1920
|
||||
mock_img.height = 1080
|
||||
mock_img.mode = 'RGBA'
|
||||
mock_img.split.return_value = [Mock(), Mock(), Mock(), Mock()]
|
||||
mock_image_open.return_value = mock_img
|
||||
|
||||
mock_stat.return_value.st_size = 1024 * 1024
|
||||
|
||||
with patch('PIL.Image.new') as mock_new:
|
||||
mock_rgb = Mock()
|
||||
mock_new.return_value = mock_rgb
|
||||
|
||||
mo.optimize_image('input.png', 'output.jpg', verbose=False)
|
||||
|
||||
mock_new.assert_called_once()
|
||||
|
||||
def test_optimize_image_no_pillow(self):
|
||||
"""Test image optimization without Pillow."""
|
||||
with patch.dict('sys.modules', {'PIL': None}):
|
||||
result = mo.optimize_image('input.jpg', 'output.jpg')
|
||||
# Will fail to import but function handles it
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestVideoSplitting:
|
||||
"""Test video splitting functionality."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
@patch('pathlib.Path.mkdir')
|
||||
def test_split_video_success(self, mock_mkdir, mock_run, mock_info, mock_check):
|
||||
"""Test successful video splitting."""
|
||||
mock_check.return_value = True
|
||||
mock_info.return_value = {'duration': 7200.0} # 2 hours
|
||||
|
||||
result = mo.split_video(
|
||||
'input.mp4',
|
||||
'./chunks',
|
||||
chunk_duration=3600, # 1 hour chunks
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Duration 7200s / 3600s = 2, +1 for safety = 3 chunks
|
||||
assert len(result) == 3
|
||||
assert mock_run.call_count == 3
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
def test_split_video_short_duration(self, mock_info, mock_check):
|
||||
"""Test splitting video shorter than chunk duration."""
|
||||
mock_check.return_value = True
|
||||
mock_info.return_value = {'duration': 1800.0} # 30 minutes
|
||||
|
||||
result = mo.split_video(
|
||||
'input.mp4',
|
||||
'./chunks',
|
||||
chunk_duration=3600, # 1 hour
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result == ['input.mp4']
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_split_video_no_ffmpeg(self, mock_check):
|
||||
"""Test video splitting without ffmpeg."""
|
||||
mock_check.return_value = False
|
||||
|
||||
result = mo.split_video('input.mp4', './chunks')
|
||||
assert result == []
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--cov=media_optimizer', '--cov-report=term-missing'])
|
||||
204
skills/better-auth/SKILL.md
Normal file
204
skills/better-auth/SKILL.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
name: better-auth
|
||||
description: Implement authentication and authorization with Better Auth - a framework-agnostic TypeScript authentication framework. Features include email/password authentication with verification, OAuth providers (Google, GitHub, Discord, etc.), two-factor authentication (TOTP, SMS), passkeys/WebAuthn support, session management, role-based access control (RBAC), rate limiting, and database adapters. Use when adding authentication to applications, implementing OAuth flows, setting up 2FA/MFA, managing user sessions, configuring authorization rules, or building secure authentication systems for web applications.
|
||||
license: MIT
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
# Better Auth Skill
|
||||
|
||||
Better Auth is comprehensive, framework-agnostic authentication/authorization framework for TypeScript with built-in email/password, social OAuth, and powerful plugin ecosystem for advanced features.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Implementing auth in TypeScript/JavaScript applications
|
||||
- Adding email/password or social OAuth authentication
|
||||
- Setting up 2FA, passkeys, magic links, advanced auth features
|
||||
- Building multi-tenant apps with organization support
|
||||
- Managing sessions and user lifecycle
|
||||
- Working with any framework (Next.js, Nuxt, SvelteKit, Remix, Astro, Hono, Express, etc.)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install better-auth
|
||||
# or pnpm/yarn/bun add better-auth
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Create `.env`:
|
||||
```env
|
||||
BETTER_AUTH_SECRET=<generated-secret-32-chars-min>
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
### Basic Server Setup
|
||||
|
||||
Create `auth.ts` (root, lib/, utils/, or under src/app/server/):
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
// See references/database-integration.md
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
autoSignIn: true
|
||||
},
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
|
||||
```bash
|
||||
npx @better-auth/cli generate # Generate schema/migrations
|
||||
npx @better-auth/cli migrate # Apply migrations (Kysely only)
|
||||
```
|
||||
|
||||
### Mount API Handler
|
||||
|
||||
**Next.js App Router:**
|
||||
```ts
|
||||
// app/api/auth/[...all]/route.ts
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { POST, GET } = toNextJsHandler(auth);
|
||||
```
|
||||
|
||||
**Other frameworks:** See references/email-password-auth.md#framework-setup
|
||||
|
||||
### Client Setup
|
||||
|
||||
Create `auth-client.ts`:
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000"
|
||||
});
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```ts
|
||||
// Sign up
|
||||
await authClient.signUp.email({
|
||||
email: "user@example.com",
|
||||
password: "secure123",
|
||||
name: "John Doe"
|
||||
});
|
||||
|
||||
// Sign in
|
||||
await authClient.signIn.email({
|
||||
email: "user@example.com",
|
||||
password: "secure123"
|
||||
});
|
||||
|
||||
// OAuth
|
||||
await authClient.signIn.social({ provider: "github" });
|
||||
|
||||
// Session
|
||||
const { data: session } = authClient.useSession(); // React/Vue/Svelte
|
||||
const { data: session } = await authClient.getSession(); // Vanilla JS
|
||||
```
|
||||
|
||||
## Feature Selection Matrix
|
||||
|
||||
| Feature | Plugin Required | Use Case | Reference |
|
||||
|---------|----------------|----------|-----------|
|
||||
| Email/Password | No (built-in) | Basic auth | [email-password-auth.md](./references/email-password-auth.md) |
|
||||
| OAuth (GitHub, Google, etc.) | No (built-in) | Social login | [oauth-providers.md](./references/oauth-providers.md) |
|
||||
| Email Verification | No (built-in) | Verify email addresses | [email-password-auth.md](./references/email-password-auth.md#email-verification) |
|
||||
| Password Reset | No (built-in) | Forgot password flow | [email-password-auth.md](./references/email-password-auth.md#password-reset) |
|
||||
| Two-Factor Auth (2FA/TOTP) | Yes (`twoFactor`) | Enhanced security | [advanced-features.md](./references/advanced-features.md#two-factor-authentication) |
|
||||
| Passkeys/WebAuthn | Yes (`passkey`) | Passwordless auth | [advanced-features.md](./references/advanced-features.md#passkeys-webauthn) |
|
||||
| Magic Link | Yes (`magicLink`) | Email-based login | [advanced-features.md](./references/advanced-features.md#magic-link) |
|
||||
| Username Auth | Yes (`username`) | Username login | [email-password-auth.md](./references/email-password-auth.md#username-authentication) |
|
||||
| Organizations/Multi-tenant | Yes (`organization`) | Team/org features | [advanced-features.md](./references/advanced-features.md#organizations) |
|
||||
| Rate Limiting | No (built-in) | Prevent abuse | [advanced-features.md](./references/advanced-features.md#rate-limiting) |
|
||||
| Session Management | No (built-in) | User sessions | [advanced-features.md](./references/advanced-features.md#session-management) |
|
||||
|
||||
## Auth Method Selection Guide
|
||||
|
||||
**Choose Email/Password when:**
|
||||
- Building standard web app with traditional auth
|
||||
- Need full control over user credentials
|
||||
- Targeting users who prefer email-based accounts
|
||||
|
||||
**Choose OAuth when:**
|
||||
- Want quick signup with minimal friction
|
||||
- Users already have social accounts
|
||||
- Need access to social profile data
|
||||
|
||||
**Choose Passkeys when:**
|
||||
- Want passwordless experience
|
||||
- Targeting modern browsers/devices
|
||||
- Security is top priority
|
||||
|
||||
**Choose Magic Link when:**
|
||||
- Want passwordless without WebAuthn complexity
|
||||
- Targeting email-first users
|
||||
- Need temporary access links
|
||||
|
||||
**Combine Multiple Methods when:**
|
||||
- Want flexibility for different user preferences
|
||||
- Building enterprise apps with various auth requirements
|
||||
- Need progressive enhancement (start simple, add more options)
|
||||
|
||||
## Core Architecture
|
||||
|
||||
Better Auth uses client-server architecture:
|
||||
1. **Server** (`better-auth`): Handles auth logic, database ops, API routes
|
||||
2. **Client** (`better-auth/client`): Provides hooks/methods for frontend
|
||||
3. **Plugins**: Extend both server/client functionality
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Install `better-auth` package
|
||||
- [ ] Set environment variables (SECRET, URL)
|
||||
- [ ] Create auth server instance with database config
|
||||
- [ ] Run schema migration (`npx @better-auth/cli generate`)
|
||||
- [ ] Mount API handler in framework
|
||||
- [ ] Create client instance
|
||||
- [ ] Implement sign-up/sign-in UI
|
||||
- [ ] Add session management to components
|
||||
- [ ] Set up protected routes/middleware
|
||||
- [ ] Add plugins as needed (regenerate schema after)
|
||||
- [ ] Test complete auth flow
|
||||
- [ ] Configure email sending (verification/reset)
|
||||
- [ ] Enable rate limiting for production
|
||||
- [ ] Set up error handling
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
### Core Authentication
|
||||
- [Email/Password Authentication](./references/email-password-auth.md) - Email/password setup, verification, password reset, username auth
|
||||
- [OAuth Providers](./references/oauth-providers.md) - Social login setup, provider configuration, token management
|
||||
- [Database Integration](./references/database-integration.md) - Database adapters, schema setup, migrations
|
||||
|
||||
### Advanced Features
|
||||
- [Advanced Features](./references/advanced-features.md) - 2FA/MFA, passkeys, magic links, organizations, rate limiting, session management
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/better_auth_init.py` - Initialize Better Auth configuration with interactive setup
|
||||
|
||||
## Resources
|
||||
|
||||
- Docs: https://www.better-auth.com/docs
|
||||
- GitHub: https://github.com/better-auth/better-auth
|
||||
- Plugins: https://www.better-auth.com/docs/plugins
|
||||
- Examples: https://www.better-auth.com/docs/examples
|
||||
553
skills/better-auth/references/advanced-features.md
Normal file
553
skills/better-auth/references/advanced-features.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# Advanced Features
|
||||
|
||||
Better Auth plugins extend functionality beyond basic authentication.
|
||||
|
||||
## Two-Factor Authentication
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: "YourAppName", // TOTP issuer name
|
||||
otpOptions: {
|
||||
period: 30, // OTP validity period (seconds)
|
||||
digits: 6, // OTP length
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { twoFactorClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
twoFactorClient({
|
||||
twoFactorPage: "/two-factor", // Redirect to 2FA verification page
|
||||
redirect: true // Auto-redirect if 2FA required
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Enable 2FA for User
|
||||
|
||||
```ts
|
||||
// Enable TOTP
|
||||
const { data } = await authClient.twoFactor.enable({
|
||||
password: "userPassword" // Verify user identity
|
||||
});
|
||||
|
||||
// data contains QR code URI for authenticator app
|
||||
const qrCodeUri = data.totpURI;
|
||||
const backupCodes = data.backupCodes; // Save these securely
|
||||
```
|
||||
|
||||
### Verify TOTP Code
|
||||
|
||||
```ts
|
||||
await authClient.twoFactor.verifyTOTP({
|
||||
code: "123456",
|
||||
trustDevice: true // Skip 2FA on this device for 30 days
|
||||
});
|
||||
```
|
||||
|
||||
### Disable 2FA
|
||||
|
||||
```ts
|
||||
await authClient.twoFactor.disable({
|
||||
password: "userPassword"
|
||||
});
|
||||
```
|
||||
|
||||
### Backup Codes
|
||||
|
||||
```ts
|
||||
// Generate new backup codes
|
||||
const { data } = await authClient.twoFactor.generateBackupCodes({
|
||||
password: "userPassword"
|
||||
});
|
||||
|
||||
// Use backup code instead of TOTP
|
||||
await authClient.twoFactor.verifyBackupCode({
|
||||
code: "backup-code-123"
|
||||
});
|
||||
```
|
||||
|
||||
## Passkeys (WebAuthn)
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { passkey } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
passkey({
|
||||
rpName: "YourApp", // Relying Party name
|
||||
rpID: "yourdomain.com" // Your domain
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { passkeyClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [passkeyClient()]
|
||||
});
|
||||
```
|
||||
|
||||
### Register Passkey
|
||||
|
||||
```ts
|
||||
// User must be authenticated first
|
||||
await authClient.passkey.register({
|
||||
name: "My Laptop" // Optional: name for this passkey
|
||||
});
|
||||
```
|
||||
|
||||
### Sign In with Passkey
|
||||
|
||||
```ts
|
||||
await authClient.passkey.signIn();
|
||||
```
|
||||
|
||||
### List User Passkeys
|
||||
|
||||
```ts
|
||||
const { data } = await authClient.passkey.list();
|
||||
// data contains array of registered passkeys
|
||||
```
|
||||
|
||||
### Delete Passkey
|
||||
|
||||
```ts
|
||||
await authClient.passkey.delete({
|
||||
id: "passkey-id"
|
||||
});
|
||||
```
|
||||
|
||||
## Magic Link
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { magicLink } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, url, token }) => {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: "Sign in to YourApp",
|
||||
html: `Click <a href="${url}">here</a> to sign in.`
|
||||
});
|
||||
},
|
||||
expiresIn: 300, // Link expires in 5 minutes (seconds)
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { magicLinkClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [magicLinkClient()]
|
||||
});
|
||||
```
|
||||
|
||||
### Send Magic Link
|
||||
|
||||
```ts
|
||||
await authClient.magicLink.sendMagicLink({
|
||||
email: "user@example.com",
|
||||
callbackURL: "/dashboard"
|
||||
});
|
||||
```
|
||||
|
||||
### Verify Magic Link
|
||||
|
||||
```ts
|
||||
// Called automatically when user clicks link
|
||||
// Token in URL query params handled by Better Auth
|
||||
await authClient.magicLink.verify({
|
||||
token: "token-from-url"
|
||||
});
|
||||
```
|
||||
|
||||
## Organizations (Multi-Tenancy)
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { organization } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
allowUserToCreateOrganization: true,
|
||||
organizationLimit: 5, // Max orgs per user
|
||||
creatorRole: "owner" // Role for org creator
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { organizationClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [organizationClient()]
|
||||
});
|
||||
```
|
||||
|
||||
### Create Organization
|
||||
|
||||
```ts
|
||||
await authClient.organization.create({
|
||||
name: "Acme Corp",
|
||||
slug: "acme", // Unique slug
|
||||
metadata: {
|
||||
industry: "Technology"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Invite Members
|
||||
|
||||
```ts
|
||||
await authClient.organization.inviteMember({
|
||||
organizationId: "org-id",
|
||||
email: "user@example.com",
|
||||
role: "member", // owner, admin, member
|
||||
message: "Join our team!" // Optional
|
||||
});
|
||||
```
|
||||
|
||||
### Accept Invitation
|
||||
|
||||
```ts
|
||||
await authClient.organization.acceptInvitation({
|
||||
invitationId: "invitation-id"
|
||||
});
|
||||
```
|
||||
|
||||
### List Organizations
|
||||
|
||||
```ts
|
||||
const { data } = await authClient.organization.list();
|
||||
// Returns user's organizations
|
||||
```
|
||||
|
||||
### Update Member Role
|
||||
|
||||
```ts
|
||||
await authClient.organization.updateMemberRole({
|
||||
organizationId: "org-id",
|
||||
userId: "user-id",
|
||||
role: "admin"
|
||||
});
|
||||
```
|
||||
|
||||
### Remove Member
|
||||
|
||||
```ts
|
||||
await authClient.organization.removeMember({
|
||||
organizationId: "org-id",
|
||||
userId: "user-id"
|
||||
});
|
||||
```
|
||||
|
||||
### Delete Organization
|
||||
|
||||
```ts
|
||||
await authClient.organization.delete({
|
||||
organizationId: "org-id"
|
||||
});
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### Configure Session Expiration
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days (seconds)
|
||||
updateAge: 60 * 60 * 24, // Update session every 24 hours
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60 // Cache for 5 minutes
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Server-Side Session
|
||||
|
||||
```ts
|
||||
// Next.js
|
||||
import { auth } from "@/lib/auth";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers()
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
// Not authenticated
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side Session
|
||||
|
||||
```tsx
|
||||
// React
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
function UserProfile() {
|
||||
const { data: session, isPending, error } = authClient.useSession();
|
||||
|
||||
if (isPending) return <div>Loading...</div>;
|
||||
if (error) return <div>Error</div>;
|
||||
if (!session) return <div>Not logged in</div>;
|
||||
|
||||
return <div>Hello, {session.user.name}!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### List Active Sessions
|
||||
|
||||
```ts
|
||||
const { data: sessions } = await authClient.listSessions();
|
||||
// Returns all active sessions for current user
|
||||
```
|
||||
|
||||
### Revoke Session
|
||||
|
||||
```ts
|
||||
await authClient.revokeSession({
|
||||
sessionId: "session-id"
|
||||
});
|
||||
```
|
||||
|
||||
### Revoke All Sessions
|
||||
|
||||
```ts
|
||||
await authClient.revokeAllSessions();
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Server Configuration
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
window: 60, // Time window in seconds
|
||||
max: 10, // Max requests per window
|
||||
storage: "memory", // "memory" or "database"
|
||||
customRules: {
|
||||
"/api/auth/sign-in": {
|
||||
window: 60,
|
||||
max: 5 // Stricter limit for sign-in
|
||||
},
|
||||
"/api/auth/sign-up": {
|
||||
window: 3600,
|
||||
max: 3 // 3 signups per hour
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Rate Limiter
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
customLimiter: async ({ request, limit }) => {
|
||||
// Custom rate limiting logic
|
||||
const ip = request.headers.get("x-forwarded-for");
|
||||
const key = `ratelimit:${ip}`;
|
||||
|
||||
// Use Redis, etc.
|
||||
const count = await redis.incr(key);
|
||||
if (count === 1) {
|
||||
await redis.expire(key, limit.window);
|
||||
}
|
||||
|
||||
if (count > limit.max) {
|
||||
throw new Error("Rate limit exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Anonymous Sessions
|
||||
|
||||
Track users before they sign up.
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { anonymous } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [anonymous()]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
// Create anonymous session
|
||||
const { data } = await authClient.signIn.anonymous();
|
||||
|
||||
// Convert to full account
|
||||
await authClient.signUp.email({
|
||||
email: "user@example.com",
|
||||
password: "password123",
|
||||
linkAnonymousSession: true // Link anonymous data
|
||||
});
|
||||
```
|
||||
|
||||
## Email OTP
|
||||
|
||||
One-time password via email (passwordless).
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { emailOTP } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
emailOTP({
|
||||
sendVerificationOTP: async ({ email, otp }) => {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: "Your verification code",
|
||||
text: `Your code is: ${otp}`
|
||||
});
|
||||
},
|
||||
expiresIn: 300, // 5 minutes
|
||||
length: 6 // OTP length
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
// Send OTP to email
|
||||
await authClient.emailOTP.sendOTP({
|
||||
email: "user@example.com"
|
||||
});
|
||||
|
||||
// Verify OTP
|
||||
await authClient.emailOTP.verifyOTP({
|
||||
email: "user@example.com",
|
||||
otp: "123456"
|
||||
});
|
||||
```
|
||||
|
||||
## Phone Number Authentication
|
||||
|
||||
Requires phone number plugin.
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { phoneNumber } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
phoneNumber({
|
||||
sendOTP: async ({ phoneNumber, otp }) => {
|
||||
// Use Twilio, AWS SNS, etc.
|
||||
await sendSMS(phoneNumber, `Your code: ${otp}`);
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
// Sign up with phone
|
||||
await authClient.signUp.phoneNumber({
|
||||
phoneNumber: "+1234567890",
|
||||
password: "password123"
|
||||
});
|
||||
|
||||
// Send OTP
|
||||
await authClient.phoneNumber.sendOTP({
|
||||
phoneNumber: "+1234567890"
|
||||
});
|
||||
|
||||
// Verify OTP
|
||||
await authClient.phoneNumber.verifyOTP({
|
||||
phoneNumber: "+1234567890",
|
||||
otp: "123456"
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **2FA**: Offer 2FA as optional, make mandatory for admin users
|
||||
2. **Passkeys**: Implement as progressive enhancement (fallback to password)
|
||||
3. **Magic Links**: Set short expiration (5-15 minutes)
|
||||
4. **Organizations**: Implement RBAC for org permissions
|
||||
5. **Sessions**: Use short expiration for sensitive apps
|
||||
6. **Rate Limiting**: Enable in production, adjust limits based on usage
|
||||
7. **Anonymous Sessions**: Clean up old anonymous sessions periodically
|
||||
8. **Backup Codes**: Force users to save backup codes before enabling 2FA
|
||||
9. **Multi-Device**: Allow users to manage trusted devices
|
||||
10. **Audit Logs**: Track sensitive operations (role changes, 2FA changes)
|
||||
|
||||
## Regenerate Schema After Plugins
|
||||
|
||||
After adding any plugin:
|
||||
|
||||
```bash
|
||||
npx @better-auth/cli generate
|
||||
npx @better-auth/cli migrate # if using Kysely
|
||||
```
|
||||
|
||||
Or manually apply migrations for your ORM (Drizzle, Prisma).
|
||||
577
skills/better-auth/references/database-integration.md
Normal file
577
skills/better-auth/references/database-integration.md
Normal file
@@ -0,0 +1,577 @@
|
||||
# Database Integration
|
||||
|
||||
Better Auth supports multiple databases and ORMs for flexible data persistence.
|
||||
|
||||
## Supported Databases
|
||||
|
||||
- SQLite
|
||||
- PostgreSQL
|
||||
- MySQL/MariaDB
|
||||
- MongoDB
|
||||
- Any database with adapter support
|
||||
|
||||
## Direct Database Connection
|
||||
|
||||
### SQLite
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: new Database("./sqlite.db"),
|
||||
// or
|
||||
database: new Database(":memory:") // In-memory for testing
|
||||
});
|
||||
```
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
// or explicit config
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
user: "postgres",
|
||||
password: "password",
|
||||
database: "myapp"
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: pool
|
||||
});
|
||||
```
|
||||
|
||||
### MySQL
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { createPool } from "mysql2/promise";
|
||||
|
||||
const pool = createPool({
|
||||
host: "localhost",
|
||||
user: "root",
|
||||
password: "password",
|
||||
database: "myapp",
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: pool
|
||||
});
|
||||
```
|
||||
|
||||
## ORM Adapters
|
||||
|
||||
### Drizzle ORM
|
||||
|
||||
**Install:**
|
||||
```bash
|
||||
npm install drizzle-orm better-auth
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL
|
||||
});
|
||||
|
||||
const db = drizzle(pool);
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg", // "pg" | "mysql" | "sqlite"
|
||||
schema: {
|
||||
// Optional: custom table names
|
||||
user: "users",
|
||||
session: "sessions",
|
||||
account: "accounts",
|
||||
verification: "verifications"
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Generate Schema:**
|
||||
```bash
|
||||
npx @better-auth/cli generate --adapter drizzle
|
||||
```
|
||||
|
||||
### Prisma
|
||||
|
||||
**Install:**
|
||||
```bash
|
||||
npm install @prisma/client better-auth
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: "postgresql", // "postgresql" | "mysql" | "sqlite"
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Generate Schema:**
|
||||
```bash
|
||||
npx @better-auth/cli generate --adapter prisma
|
||||
```
|
||||
|
||||
**Apply to Prisma:**
|
||||
```bash
|
||||
# Add generated schema to schema.prisma
|
||||
npx prisma migrate dev --name init
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### Kysely
|
||||
|
||||
**Install:**
|
||||
```bash
|
||||
npm install kysely better-auth
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { kyselyAdapter } from "better-auth/adapters/kysely";
|
||||
import { Kysely, PostgresDialect } from "kysely";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const db = new Kysely({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
connectionString: process.env.DATABASE_URL
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: kyselyAdapter(db, {
|
||||
provider: "pg"
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Auto-migrate with Kysely:**
|
||||
```bash
|
||||
npx @better-auth/cli migrate --adapter kysely
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
**Install:**
|
||||
```bash
|
||||
npm install mongodb better-auth
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
const client = new MongoClient(process.env.MONGODB_URI!);
|
||||
await client.connect();
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: mongodbAdapter(client, {
|
||||
databaseName: "myapp"
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Generate Collections:**
|
||||
```bash
|
||||
npx @better-auth/cli generate --adapter mongodb
|
||||
```
|
||||
|
||||
## Core Database Schema
|
||||
|
||||
Better Auth requires these core tables/collections:
|
||||
|
||||
### User Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE user (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
emailVerified BOOLEAN DEFAULT FALSE,
|
||||
name TEXT,
|
||||
image TEXT,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Session Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE session (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
expiresAt TIMESTAMP NOT NULL,
|
||||
ipAddress TEXT,
|
||||
userAgent TEXT,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### Account Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE account (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
accountId TEXT NOT NULL,
|
||||
providerId TEXT NOT NULL,
|
||||
accessToken TEXT,
|
||||
refreshToken TEXT,
|
||||
expiresAt TIMESTAMP,
|
||||
scope TEXT,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE,
|
||||
UNIQUE(providerId, accountId)
|
||||
);
|
||||
```
|
||||
|
||||
### Verification Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE verification (
|
||||
id TEXT PRIMARY KEY,
|
||||
identifier TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
expiresAt TIMESTAMP NOT NULL,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Schema Generation
|
||||
|
||||
### Using CLI
|
||||
|
||||
```bash
|
||||
# Generate schema files
|
||||
npx @better-auth/cli generate
|
||||
|
||||
# Specify adapter
|
||||
npx @better-auth/cli generate --adapter drizzle
|
||||
npx @better-auth/cli generate --adapter prisma
|
||||
|
||||
# Specify output
|
||||
npx @better-auth/cli generate --output ./db/schema.ts
|
||||
```
|
||||
|
||||
### Auto-migrate (Kysely only)
|
||||
|
||||
```bash
|
||||
npx @better-auth/cli migrate
|
||||
```
|
||||
|
||||
For other ORMs, apply generated schema manually.
|
||||
|
||||
## Custom Fields
|
||||
|
||||
Add custom fields to user table:
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
user: {
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: "user"
|
||||
},
|
||||
phoneNumber: {
|
||||
type: "string",
|
||||
required: false
|
||||
},
|
||||
subscriptionTier: {
|
||||
type: "string",
|
||||
required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
After adding fields:
|
||||
```bash
|
||||
npx @better-auth/cli generate
|
||||
```
|
||||
|
||||
Update user with custom fields:
|
||||
```ts
|
||||
await authClient.updateUser({
|
||||
role: "admin",
|
||||
phoneNumber: "+1234567890"
|
||||
});
|
||||
```
|
||||
|
||||
## Plugin Schema Extensions
|
||||
|
||||
Plugins add their own tables/fields. Regenerate schema after adding plugins:
|
||||
|
||||
```bash
|
||||
npx @better-auth/cli generate
|
||||
```
|
||||
|
||||
### Two-Factor Plugin Tables
|
||||
|
||||
- `twoFactor`: Stores TOTP secrets, backup codes
|
||||
|
||||
### Passkey Plugin Tables
|
||||
|
||||
- `passkey`: Stores WebAuthn credentials
|
||||
|
||||
### Organization Plugin Tables
|
||||
|
||||
- `organization`: Organization data
|
||||
- `member`: Organization members
|
||||
- `invitation`: Pending invitations
|
||||
|
||||
## Migration Strategies
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Generate schema
|
||||
npx @better-auth/cli generate
|
||||
|
||||
# Apply migrations (Kysely)
|
||||
npx @better-auth/cli migrate
|
||||
|
||||
# Or manual (Prisma)
|
||||
npx prisma migrate dev
|
||||
|
||||
# Or manual (Drizzle)
|
||||
npx drizzle-kit push
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
# Review generated migration
|
||||
npx @better-auth/cli generate
|
||||
|
||||
# Test in staging
|
||||
# Apply to production with your ORM's migration tool
|
||||
|
||||
# Prisma
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Drizzle
|
||||
npx drizzle-kit push
|
||||
|
||||
# Kysely
|
||||
npx @better-auth/cli migrate
|
||||
```
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```ts
|
||||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 20, // Max connections
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
```
|
||||
|
||||
### MySQL
|
||||
|
||||
```ts
|
||||
import { createPool } from "mysql2/promise";
|
||||
|
||||
const pool = createPool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
```
|
||||
|
||||
## Database URLs
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```env
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||
# Or with connection params
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/dbname?schema=public&connection_limit=10
|
||||
```
|
||||
|
||||
### MySQL
|
||||
|
||||
```env
|
||||
DATABASE_URL=mysql://user:password@localhost:3306/dbname
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
```env
|
||||
DATABASE_URL=file:./dev.db
|
||||
# Or in-memory
|
||||
DATABASE_URL=:memory:
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
```env
|
||||
MONGODB_URI=mongodb://localhost:27017/dbname
|
||||
# Or Atlas
|
||||
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/dbname
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Indexes
|
||||
|
||||
Better Auth CLI auto-generates essential indexes:
|
||||
- `user.email` (unique)
|
||||
- `session.userId`
|
||||
- `account.userId`
|
||||
- `account.providerId, accountId` (unique)
|
||||
|
||||
Add custom indexes for performance:
|
||||
```sql
|
||||
CREATE INDEX idx_session_expires ON session(expiresAt);
|
||||
CREATE INDEX idx_user_created ON user(createdAt);
|
||||
```
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```ts
|
||||
// Use connection pooling
|
||||
// Enable query caching where applicable
|
||||
// Monitor slow queries
|
||||
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
httpOnly: true
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Backup Strategies
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
pg_dump dbname > backup.sql
|
||||
|
||||
# Restore
|
||||
psql dbname < backup.sql
|
||||
```
|
||||
|
||||
### MySQL
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
mysqldump -u root -p dbname > backup.sql
|
||||
|
||||
# Restore
|
||||
mysql -u root -p dbname < backup.sql
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
```bash
|
||||
# Copy file
|
||||
cp dev.db dev.db.backup
|
||||
|
||||
# Or use backup command
|
||||
sqlite3 dev.db ".backup backup.db"
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
mongodump --db=dbname --out=./backup
|
||||
|
||||
# Restore
|
||||
mongorestore --db=dbname ./backup/dbname
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Environment Variables**: Store credentials in env vars, never commit
|
||||
2. **Connection Pooling**: Use pools for PostgreSQL/MySQL in production
|
||||
3. **Migrations**: Use ORM migration tools, not raw SQL in production
|
||||
4. **Indexes**: Add indexes for frequently queried fields
|
||||
5. **Backups**: Automate daily backups in production
|
||||
6. **SSL**: Use SSL/TLS for database connections in production
|
||||
7. **Schema Sync**: Keep schema in sync across environments
|
||||
8. **Testing**: Use separate database for tests (in-memory SQLite ideal)
|
||||
9. **Monitoring**: Monitor query performance and connection pool usage
|
||||
10. **Cleanup**: Periodically clean expired sessions/verifications
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Errors
|
||||
|
||||
```ts
|
||||
// Add connection timeout
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
connectionTimeoutMillis: 5000
|
||||
});
|
||||
```
|
||||
|
||||
### Schema Mismatch
|
||||
|
||||
```bash
|
||||
# Regenerate schema
|
||||
npx @better-auth/cli generate
|
||||
|
||||
# Apply migrations
|
||||
# For Prisma: npx prisma migrate dev
|
||||
# For Drizzle: npx drizzle-kit push
|
||||
```
|
||||
|
||||
### Migration Failures
|
||||
|
||||
- Check database credentials
|
||||
- Verify database server is running
|
||||
- Check for schema conflicts
|
||||
- Review migration SQL manually
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- Add indexes on foreign keys
|
||||
- Enable connection pooling
|
||||
- Monitor slow queries
|
||||
- Consider read replicas for heavy read workloads
|
||||
416
skills/better-auth/references/email-password-auth.md
Normal file
416
skills/better-auth/references/email-password-auth.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Email/Password Authentication
|
||||
|
||||
Email/password is built-in auth method in Better Auth. No plugins required for basic functionality.
|
||||
|
||||
## Server Configuration
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
autoSignIn: true, // Auto sign-in after signup (default: true)
|
||||
requireEmailVerification: false, // Require email verification before login
|
||||
sendResetPasswordToken: async ({ user, url }) => {
|
||||
// Send password reset email
|
||||
await sendEmail(user.email, url);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Password Requirements
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
password: {
|
||||
minLength: 8,
|
||||
requireUppercase: true,
|
||||
requireLowercase: true,
|
||||
requireNumbers: true,
|
||||
requireSpecialChars: true
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Client Usage
|
||||
|
||||
### Sign Up
|
||||
|
||||
```ts
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: "user@example.com",
|
||||
password: "securePassword123",
|
||||
name: "John Doe",
|
||||
image: "https://example.com/avatar.jpg", // optional
|
||||
callbackURL: "/dashboard" // optional
|
||||
}, {
|
||||
onSuccess: (ctx) => {
|
||||
// ctx.data contains user and session
|
||||
console.log("User created:", ctx.data.user);
|
||||
},
|
||||
onError: (ctx) => {
|
||||
alert(ctx.error.message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Sign In
|
||||
|
||||
```ts
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: "user@example.com",
|
||||
password: "securePassword123",
|
||||
callbackURL: "/dashboard",
|
||||
rememberMe: true // default: true
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
// redirect or update UI
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.error(ctx.error.message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Sign Out
|
||||
|
||||
```ts
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Email Verification
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url, token }) => {
|
||||
// Send verification email
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Verify your email",
|
||||
html: `Click <a href="${url}">here</a> to verify your email.`
|
||||
});
|
||||
},
|
||||
sendOnSignUp: true, // Send verification email on signup
|
||||
autoSignInAfterVerification: true // Auto sign-in after verification
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true // Require verification before login
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
// Send verification email
|
||||
await authClient.sendVerificationEmail({
|
||||
email: "user@example.com",
|
||||
callbackURL: "/verify-success"
|
||||
});
|
||||
|
||||
// Verify email with token
|
||||
await authClient.verifyEmail({
|
||||
token: "verification-token-from-email"
|
||||
});
|
||||
```
|
||||
|
||||
## Password Reset Flow
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
sendResetPasswordToken: async ({ user, url, token }) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Reset your password",
|
||||
html: `Click <a href="${url}">here</a> to reset your password.`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client Flow
|
||||
|
||||
```ts
|
||||
// Step 1: Request password reset
|
||||
await authClient.forgetPassword({
|
||||
email: "user@example.com",
|
||||
redirectTo: "/reset-password"
|
||||
});
|
||||
|
||||
// Step 2: Reset password with token
|
||||
await authClient.resetPassword({
|
||||
token: "reset-token-from-email",
|
||||
password: "newSecurePassword123"
|
||||
});
|
||||
```
|
||||
|
||||
### Change Password (Authenticated)
|
||||
|
||||
```ts
|
||||
await authClient.changePassword({
|
||||
currentPassword: "oldPassword123",
|
||||
newPassword: "newPassword456",
|
||||
revokeOtherSessions: true // Optional: logout other sessions
|
||||
});
|
||||
```
|
||||
|
||||
## Username Authentication
|
||||
|
||||
Requires `username` plugin for username-based auth.
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { username } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
username({
|
||||
// Allow sign in with username or email
|
||||
allowUsernameOrEmail: true
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { usernameClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [usernameClient()]
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
// Sign up with username
|
||||
await authClient.signUp.username({
|
||||
username: "johndoe",
|
||||
password: "securePassword123",
|
||||
email: "john@example.com", // optional
|
||||
name: "John Doe"
|
||||
});
|
||||
|
||||
// Sign in with username
|
||||
await authClient.signIn.username({
|
||||
username: "johndoe",
|
||||
password: "securePassword123"
|
||||
});
|
||||
|
||||
// Sign in with username or email (if allowUsernameOrEmail: true)
|
||||
await authClient.signIn.username({
|
||||
username: "johndoe", // or "john@example.com"
|
||||
password: "securePassword123"
|
||||
});
|
||||
```
|
||||
|
||||
## Framework Setup
|
||||
|
||||
### Next.js (App Router)
|
||||
|
||||
```ts
|
||||
// app/api/auth/[...all]/route.ts
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { POST, GET } = toNextJsHandler(auth);
|
||||
```
|
||||
|
||||
### Next.js (Pages Router)
|
||||
|
||||
```ts
|
||||
// pages/api/auth/[...all].ts
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export default toNextJsHandler(auth);
|
||||
```
|
||||
|
||||
### Nuxt
|
||||
|
||||
```ts
|
||||
// server/api/auth/[...all].ts
|
||||
import { auth } from "~/utils/auth";
|
||||
import { toWebRequest } from "better-auth/utils/web";
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
return auth.handler(toWebRequest(event));
|
||||
});
|
||||
```
|
||||
|
||||
### SvelteKit
|
||||
|
||||
```ts
|
||||
// hooks.server.ts
|
||||
import { auth } from "$lib/auth";
|
||||
import { svelteKitHandler } from "better-auth/svelte-kit";
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
return svelteKitHandler({ event, resolve, auth });
|
||||
}
|
||||
```
|
||||
|
||||
### Astro
|
||||
|
||||
```ts
|
||||
// pages/api/auth/[...all].ts
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export async function ALL({ request }: { request: Request }) {
|
||||
return auth.handler(request);
|
||||
}
|
||||
```
|
||||
|
||||
### Hono
|
||||
|
||||
```ts
|
||||
import { Hono } from "hono";
|
||||
import { auth } from "./auth";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/*", (c) => {
|
||||
return auth.handler(c.req.raw);
|
||||
});
|
||||
```
|
||||
|
||||
### Express
|
||||
|
||||
```ts
|
||||
import express from "express";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
import { auth } from "./auth";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.all("/api/auth/*", toNodeHandler(auth));
|
||||
```
|
||||
|
||||
## Protected Routes
|
||||
|
||||
### Next.js Middleware
|
||||
|
||||
```ts
|
||||
// middleware.ts
|
||||
import { auth } from "@/lib/auth";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.redirect(new URL("/login", request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/dashboard/:path*", "/profile/:path*"]
|
||||
};
|
||||
```
|
||||
|
||||
### SvelteKit Hooks
|
||||
|
||||
```ts
|
||||
// hooks.server.ts
|
||||
import { auth } from "$lib/auth";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: event.request.headers
|
||||
});
|
||||
|
||||
if (event.url.pathname.startsWith("/dashboard") && !session) {
|
||||
throw redirect(303, "/login");
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
}
|
||||
```
|
||||
|
||||
### Nuxt Middleware
|
||||
|
||||
```ts
|
||||
// middleware/auth.ts
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const { data: session } = await useAuthSession();
|
||||
|
||||
if (!session.value && to.path.startsWith("/dashboard")) {
|
||||
return navigateTo("/login");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## User Profile Management
|
||||
|
||||
### Get Current User
|
||||
|
||||
```ts
|
||||
const { data: session } = await authClient.getSession();
|
||||
console.log(session.user);
|
||||
```
|
||||
|
||||
### Update User Profile
|
||||
|
||||
```ts
|
||||
await authClient.updateUser({
|
||||
name: "New Name",
|
||||
image: "https://example.com/new-avatar.jpg",
|
||||
// Custom fields if defined in schema
|
||||
});
|
||||
```
|
||||
|
||||
### Delete User Account
|
||||
|
||||
```ts
|
||||
await authClient.deleteUser({
|
||||
password: "currentPassword", // Required for security
|
||||
callbackURL: "/" // Redirect after deletion
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Password Security**: Enforce strong password requirements
|
||||
2. **Email Verification**: Enable for production to prevent spam
|
||||
3. **Rate Limiting**: Prevent brute force attacks (see advanced-features.md)
|
||||
4. **HTTPS**: Always use HTTPS in production
|
||||
5. **Error Messages**: Don't reveal if email exists during login
|
||||
6. **Session Security**: Use secure, httpOnly cookies
|
||||
7. **CSRF Protection**: Better Auth handles this automatically
|
||||
8. **Password Reset**: Set short expiration for reset tokens
|
||||
9. **Account Lockout**: Consider implementing after N failed attempts
|
||||
10. **Audit Logs**: Track auth events for security monitoring
|
||||
430
skills/better-auth/references/oauth-providers.md
Normal file
430
skills/better-auth/references/oauth-providers.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# OAuth Providers
|
||||
|
||||
Better Auth provides built-in OAuth 2.0 support for social authentication. No plugins required.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
GitHub, Google, Apple, Discord, Facebook, Microsoft, Twitter/X, Spotify, Twitch, LinkedIn, Dropbox, GitLab, and more.
|
||||
|
||||
## Basic OAuth Setup
|
||||
|
||||
### Server Configuration
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
// Optional: custom scopes
|
||||
scope: ["user:email", "read:user"]
|
||||
},
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
scope: ["openid", "email", "profile"]
|
||||
},
|
||||
discord: {
|
||||
clientId: process.env.DISCORD_CLIENT_ID!,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
// Basic sign in
|
||||
await authClient.signIn.social({
|
||||
provider: "github",
|
||||
callbackURL: "/dashboard"
|
||||
});
|
||||
|
||||
// With callbacks
|
||||
await authClient.signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
errorCallbackURL: "/error",
|
||||
newUserCallbackURL: "/welcome", // For first-time users
|
||||
});
|
||||
```
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
### GitHub OAuth
|
||||
|
||||
1. Create OAuth App at https://github.com/settings/developers
|
||||
2. Set Authorization callback URL: `http://localhost:3000/api/auth/callback/github`
|
||||
3. Add credentials to `.env`:
|
||||
|
||||
```env
|
||||
GITHUB_CLIENT_ID=your_client_id
|
||||
GITHUB_CLIENT_SECRET=your_client_secret
|
||||
```
|
||||
|
||||
### Google OAuth
|
||||
|
||||
1. Create project at https://console.cloud.google.com
|
||||
2. Enable Google+ API
|
||||
3. Create OAuth 2.0 credentials
|
||||
4. Add authorized redirect URI: `http://localhost:3000/api/auth/callback/google`
|
||||
5. Add credentials to `.env`:
|
||||
|
||||
```env
|
||||
GOOGLE_CLIENT_ID=your_client_id.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=your_client_secret
|
||||
```
|
||||
|
||||
### Discord OAuth
|
||||
|
||||
1. Create application at https://discord.com/developers/applications
|
||||
2. Add OAuth2 redirect: `http://localhost:3000/api/auth/callback/discord`
|
||||
3. Add credentials:
|
||||
|
||||
```env
|
||||
DISCORD_CLIENT_ID=your_client_id
|
||||
DISCORD_CLIENT_SECRET=your_client_secret
|
||||
```
|
||||
|
||||
### Apple Sign In
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
apple: {
|
||||
clientId: process.env.APPLE_CLIENT_ID!,
|
||||
clientSecret: process.env.APPLE_CLIENT_SECRET!,
|
||||
teamId: process.env.APPLE_TEAM_ID!,
|
||||
keyId: process.env.APPLE_KEY_ID!,
|
||||
privateKey: process.env.APPLE_PRIVATE_KEY!
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Microsoft/Azure AD
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
microsoft: {
|
||||
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
||||
tenantId: process.env.MICROSOFT_TENANT_ID, // Optional: for specific tenant
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Twitter/X OAuth
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
twitter: {
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Custom OAuth Provider
|
||||
|
||||
Add custom OAuth 2.0 provider:
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
customProvider: {
|
||||
clientId: process.env.CUSTOM_CLIENT_ID!,
|
||||
clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
|
||||
authorizationUrl: "https://provider.com/oauth/authorize",
|
||||
tokenUrl: "https://provider.com/oauth/token",
|
||||
userInfoUrl: "https://provider.com/oauth/userinfo",
|
||||
scope: ["email", "profile"],
|
||||
// Map provider user data to Better Auth user
|
||||
mapProfile: (profile) => ({
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
name: profile.name,
|
||||
image: profile.avatar_url
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Account Linking
|
||||
|
||||
Link multiple OAuth providers to same user account.
|
||||
|
||||
### Server Setup
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: ["google", "github"] // Auto-link these providers
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client Usage
|
||||
|
||||
```ts
|
||||
// Link new provider to existing account
|
||||
await authClient.linkSocial({
|
||||
provider: "google",
|
||||
callbackURL: "/profile"
|
||||
});
|
||||
|
||||
// List linked accounts
|
||||
const { data: session } = await authClient.getSession();
|
||||
const accounts = session.user.accounts;
|
||||
|
||||
// Unlink account
|
||||
await authClient.unlinkAccount({
|
||||
accountId: "account-id"
|
||||
});
|
||||
```
|
||||
|
||||
## Token Management
|
||||
|
||||
### Access OAuth Tokens
|
||||
|
||||
```ts
|
||||
// Server-side
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
const accounts = await auth.api.listAccounts({
|
||||
userId: session.user.id
|
||||
});
|
||||
|
||||
// Get specific provider token
|
||||
const githubAccount = accounts.find(a => a.providerId === "github");
|
||||
const accessToken = githubAccount.accessToken;
|
||||
const refreshToken = githubAccount.refreshToken;
|
||||
```
|
||||
|
||||
### Refresh Tokens
|
||||
|
||||
```ts
|
||||
// Manually refresh OAuth token
|
||||
const newToken = await auth.api.refreshToken({
|
||||
accountId: "account-id"
|
||||
});
|
||||
```
|
||||
|
||||
### Use Provider API
|
||||
|
||||
```ts
|
||||
// Example: Use GitHub token to fetch repos
|
||||
const githubAccount = accounts.find(a => a.providerId === "github");
|
||||
|
||||
const response = await fetch("https://api.github.com/user/repos", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${githubAccount.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
const repos = await response.json();
|
||||
```
|
||||
|
||||
## Advanced OAuth Configuration
|
||||
|
||||
### Custom Scopes
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
scope: [
|
||||
"user:email",
|
||||
"read:user",
|
||||
"repo", // Access repositories
|
||||
"gist" // Access gists
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### State Parameter
|
||||
|
||||
Better Auth automatically handles OAuth state parameter for CSRF protection.
|
||||
|
||||
```ts
|
||||
// Custom state validation
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
generateState: async () => {
|
||||
// Custom state generation
|
||||
return crypto.randomUUID();
|
||||
},
|
||||
validateState: async (state: string) => {
|
||||
// Custom state validation
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### PKCE Support
|
||||
|
||||
Better Auth automatically uses PKCE (Proof Key for Code Exchange) for supported providers.
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
customProvider: {
|
||||
pkce: true, // Enable PKCE
|
||||
// ... other config
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Client-Side
|
||||
|
||||
```ts
|
||||
await authClient.signIn.social({
|
||||
provider: "github",
|
||||
errorCallbackURL: "/auth/error"
|
||||
}, {
|
||||
onError: (ctx) => {
|
||||
console.error("OAuth error:", ctx.error);
|
||||
// Handle specific errors
|
||||
if (ctx.error.code === "OAUTH_ACCOUNT_ALREADY_LINKED") {
|
||||
alert("This account is already linked to another user");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Server-Side
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
callbacks: {
|
||||
async onOAuthError({ error, provider }) {
|
||||
console.error(`OAuth error with ${provider}:`, error);
|
||||
// Log to monitoring service
|
||||
await logError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Callback URLs
|
||||
|
||||
### Development
|
||||
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/{provider}
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```
|
||||
https://yourdomain.com/api/auth/callback/{provider}
|
||||
```
|
||||
|
||||
**Important:** Add all callback URLs to OAuth provider settings.
|
||||
|
||||
## UI Components
|
||||
|
||||
### Sign In Button (React)
|
||||
|
||||
```tsx
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export function SocialSignIn() {
|
||||
const handleOAuth = async (provider: string) => {
|
||||
await authClient.signIn.social({
|
||||
provider,
|
||||
callbackURL: "/dashboard"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<button onClick={() => handleOAuth("github")}>
|
||||
Sign in with GitHub
|
||||
</button>
|
||||
<button onClick={() => handleOAuth("google")}>
|
||||
Sign in with Google
|
||||
</button>
|
||||
<button onClick={() => handleOAuth("discord")}>
|
||||
Sign in with Discord
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Callback URLs**: Add all environments (dev, staging, prod) to OAuth app
|
||||
2. **Scopes**: Request minimum scopes needed
|
||||
3. **Token Storage**: Better Auth stores tokens securely in database
|
||||
4. **Token Refresh**: Implement automatic token refresh for long-lived sessions
|
||||
5. **Account Linking**: Enable for better UX when user signs in with different providers
|
||||
6. **Error Handling**: Provide clear error messages for OAuth failures
|
||||
7. **Provider Icons**: Use official brand assets for OAuth buttons
|
||||
8. **Mobile Deep Links**: Configure deep links for mobile OAuth flows
|
||||
9. **Email Matching**: Consider auto-linking accounts with same email
|
||||
10. **Privacy**: Inform users what data you access from OAuth providers
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Redirect URI Mismatch
|
||||
|
||||
Ensure callback URL in OAuth app matches exactly:
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/github
|
||||
```
|
||||
|
||||
### Missing Scopes
|
||||
|
||||
Add required scopes for email access:
|
||||
```ts
|
||||
scope: ["user:email"] // GitHub
|
||||
scope: ["email"] // Google
|
||||
```
|
||||
|
||||
### HTTPS Required
|
||||
|
||||
Some providers (Apple, Microsoft) require HTTPS callbacks. Use ngrok for local development:
|
||||
```bash
|
||||
ngrok http 3000
|
||||
```
|
||||
|
||||
### CORS Errors
|
||||
|
||||
Configure CORS if frontend/backend on different domains:
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
corsOptions: {
|
||||
origin: ["https://yourdomain.com"],
|
||||
credentials: true
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
521
skills/better-auth/scripts/better_auth_init.py
Normal file
521
skills/better-auth/scripts/better_auth_init.py
Normal file
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Better Auth Initialization Script
|
||||
|
||||
Interactive script to initialize Better Auth configuration.
|
||||
Supports multiple databases, ORMs, and authentication methods.
|
||||
|
||||
.env loading order: process.env > skill/.env > skills/.env > .claude/.env
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvConfig:
|
||||
"""Environment configuration holder."""
|
||||
secret: str
|
||||
url: str
|
||||
database_url: Optional[str] = None
|
||||
github_client_id: Optional[str] = None
|
||||
github_client_secret: Optional[str] = None
|
||||
google_client_id: Optional[str] = None
|
||||
google_client_secret: Optional[str] = None
|
||||
|
||||
|
||||
class BetterAuthInit:
|
||||
"""Better Auth configuration initializer."""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
"""
|
||||
Initialize the Better Auth configuration tool.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory. Auto-detected if not provided.
|
||||
"""
|
||||
self.project_root = project_root or self._find_project_root()
|
||||
self.env_config: Optional[EnvConfig] = None
|
||||
|
||||
@staticmethod
|
||||
def _find_project_root() -> Path:
|
||||
"""
|
||||
Find project root by looking for package.json.
|
||||
|
||||
Returns:
|
||||
Path to project root.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If project root cannot be found.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
while current != current.parent:
|
||||
if (current / "package.json").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
|
||||
raise RuntimeError("Could not find project root (no package.json found)")
|
||||
|
||||
def _load_env_files(self) -> Dict[str, str]:
|
||||
"""
|
||||
Load environment variables from .env files in order.
|
||||
|
||||
Loading order: process.env > skill/.env > skills/.env > .claude/.env
|
||||
|
||||
Returns:
|
||||
Dictionary of environment variables.
|
||||
"""
|
||||
env_vars = {}
|
||||
|
||||
# Define search paths in reverse priority order
|
||||
skill_dir = Path(__file__).parent.parent
|
||||
env_paths = [
|
||||
self.project_root / ".claude" / ".env",
|
||||
self.project_root / ".claude" / "skills" / ".env",
|
||||
skill_dir / ".env",
|
||||
]
|
||||
|
||||
# Load from files (lowest priority first)
|
||||
for env_path in env_paths:
|
||||
if env_path.exists():
|
||||
env_vars.update(self._parse_env_file(env_path))
|
||||
|
||||
# Override with process environment (highest priority)
|
||||
env_vars.update(os.environ)
|
||||
|
||||
return env_vars
|
||||
|
||||
@staticmethod
|
||||
def _parse_env_file(path: Path) -> Dict[str, str]:
|
||||
"""
|
||||
Parse .env file into dictionary.
|
||||
|
||||
Args:
|
||||
path: Path to .env file.
|
||||
|
||||
Returns:
|
||||
Dictionary of key-value pairs.
|
||||
"""
|
||||
env_vars = {}
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
# Remove quotes if present
|
||||
value = value.strip().strip('"').strip("'")
|
||||
env_vars[key.strip()] = value
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse {path}: {e}")
|
||||
|
||||
return env_vars
|
||||
|
||||
@staticmethod
|
||||
def generate_secret(length: int = 32) -> str:
|
||||
"""
|
||||
Generate cryptographically secure random secret.
|
||||
|
||||
Args:
|
||||
length: Length of secret in bytes.
|
||||
|
||||
Returns:
|
||||
Hex-encoded secret string.
|
||||
"""
|
||||
return secrets.token_hex(length)
|
||||
|
||||
def prompt_database(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Prompt user for database configuration.
|
||||
|
||||
Returns:
|
||||
Database configuration dictionary.
|
||||
"""
|
||||
print("\nDatabase Configuration")
|
||||
print("=" * 50)
|
||||
print("1. Direct Connection (PostgreSQL/MySQL/SQLite)")
|
||||
print("2. Drizzle ORM")
|
||||
print("3. Prisma")
|
||||
print("4. Kysely")
|
||||
print("5. MongoDB")
|
||||
|
||||
choice = input("\nSelect database option (1-5): ").strip()
|
||||
|
||||
db_configs = {
|
||||
"1": self._prompt_direct_db,
|
||||
"2": self._prompt_drizzle,
|
||||
"3": self._prompt_prisma,
|
||||
"4": self._prompt_kysely,
|
||||
"5": self._prompt_mongodb,
|
||||
}
|
||||
|
||||
handler = db_configs.get(choice)
|
||||
if not handler:
|
||||
print("Invalid choice. Defaulting to direct PostgreSQL.")
|
||||
return self._prompt_direct_db()
|
||||
|
||||
return handler()
|
||||
|
||||
def _prompt_direct_db(self) -> Dict[str, Any]:
|
||||
"""Prompt for direct database connection."""
|
||||
print("\nDatabase Type:")
|
||||
print("1. PostgreSQL")
|
||||
print("2. MySQL")
|
||||
print("3. SQLite")
|
||||
|
||||
db_type = input("Select (1-3): ").strip()
|
||||
|
||||
if db_type == "3":
|
||||
db_path = input("SQLite file path [./dev.db]: ").strip() or "./dev.db"
|
||||
return {
|
||||
"type": "sqlite",
|
||||
"import": "import Database from 'better-sqlite3';",
|
||||
"config": f'database: new Database("{db_path}")'
|
||||
}
|
||||
elif db_type == "2":
|
||||
db_url = input("MySQL connection string: ").strip()
|
||||
return {
|
||||
"type": "mysql",
|
||||
"import": "import { createPool } from 'mysql2/promise';",
|
||||
"config": f"database: createPool({{ connectionString: process.env.DATABASE_URL }})",
|
||||
"env_var": ("DATABASE_URL", db_url)
|
||||
}
|
||||
else:
|
||||
db_url = input("PostgreSQL connection string: ").strip()
|
||||
return {
|
||||
"type": "postgresql",
|
||||
"import": "import { Pool } from 'pg';",
|
||||
"config": "database: new Pool({ connectionString: process.env.DATABASE_URL })",
|
||||
"env_var": ("DATABASE_URL", db_url)
|
||||
}
|
||||
|
||||
def _prompt_drizzle(self) -> Dict[str, Any]:
|
||||
"""Prompt for Drizzle ORM configuration."""
|
||||
print("\nDrizzle Provider:")
|
||||
print("1. PostgreSQL")
|
||||
print("2. MySQL")
|
||||
print("3. SQLite")
|
||||
|
||||
provider = input("Select (1-3): ").strip()
|
||||
provider_map = {"1": "pg", "2": "mysql", "3": "sqlite"}
|
||||
provider_name = provider_map.get(provider, "pg")
|
||||
|
||||
return {
|
||||
"type": "drizzle",
|
||||
"provider": provider_name,
|
||||
"import": "import { drizzleAdapter } from 'better-auth/adapters/drizzle';\nimport { db } from '@/db';",
|
||||
"config": f"database: drizzleAdapter(db, {{ provider: '{provider_name}' }})"
|
||||
}
|
||||
|
||||
def _prompt_prisma(self) -> Dict[str, Any]:
|
||||
"""Prompt for Prisma configuration."""
|
||||
print("\nPrisma Provider:")
|
||||
print("1. PostgreSQL")
|
||||
print("2. MySQL")
|
||||
print("3. SQLite")
|
||||
|
||||
provider = input("Select (1-3): ").strip()
|
||||
provider_map = {"1": "postgresql", "2": "mysql", "3": "sqlite"}
|
||||
provider_name = provider_map.get(provider, "postgresql")
|
||||
|
||||
return {
|
||||
"type": "prisma",
|
||||
"provider": provider_name,
|
||||
"import": "import { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { PrismaClient } from '@prisma/client';\n\nconst prisma = new PrismaClient();",
|
||||
"config": f"database: prismaAdapter(prisma, {{ provider: '{provider_name}' }})"
|
||||
}
|
||||
|
||||
def _prompt_kysely(self) -> Dict[str, Any]:
|
||||
"""Prompt for Kysely configuration."""
|
||||
return {
|
||||
"type": "kysely",
|
||||
"import": "import { kyselyAdapter } from 'better-auth/adapters/kysely';\nimport { db } from '@/db';",
|
||||
"config": "database: kyselyAdapter(db, { provider: 'pg' })"
|
||||
}
|
||||
|
||||
def _prompt_mongodb(self) -> Dict[str, Any]:
|
||||
"""Prompt for MongoDB configuration."""
|
||||
mongo_uri = input("MongoDB connection string: ").strip()
|
||||
db_name = input("Database name: ").strip()
|
||||
|
||||
return {
|
||||
"type": "mongodb",
|
||||
"import": "import { mongodbAdapter } from 'better-auth/adapters/mongodb';\nimport { client } from '@/db';",
|
||||
"config": f"database: mongodbAdapter(client, {{ databaseName: '{db_name}' }})",
|
||||
"env_var": ("MONGODB_URI", mongo_uri)
|
||||
}
|
||||
|
||||
def prompt_auth_methods(self) -> List[str]:
|
||||
"""
|
||||
Prompt user for authentication methods.
|
||||
|
||||
Returns:
|
||||
List of selected auth method codes.
|
||||
"""
|
||||
print("\nAuthentication Methods")
|
||||
print("=" * 50)
|
||||
print("Select authentication methods (space-separated, e.g., '1 2 3'):")
|
||||
print("1. Email/Password")
|
||||
print("2. GitHub OAuth")
|
||||
print("3. Google OAuth")
|
||||
print("4. Discord OAuth")
|
||||
print("5. Two-Factor Authentication (2FA)")
|
||||
print("6. Passkeys (WebAuthn)")
|
||||
print("7. Magic Link")
|
||||
print("8. Username")
|
||||
|
||||
choices = input("\nYour selection: ").strip().split()
|
||||
return [c for c in choices if c in "12345678"]
|
||||
|
||||
def generate_auth_config(
|
||||
self,
|
||||
db_config: Dict[str, Any],
|
||||
auth_methods: List[str],
|
||||
) -> str:
|
||||
"""
|
||||
Generate auth.ts configuration file content.
|
||||
|
||||
Args:
|
||||
db_config: Database configuration.
|
||||
auth_methods: Selected authentication methods.
|
||||
|
||||
Returns:
|
||||
Generated TypeScript configuration code.
|
||||
"""
|
||||
imports = ["import { betterAuth } from 'better-auth';"]
|
||||
plugins = []
|
||||
plugin_imports = []
|
||||
config_parts = []
|
||||
|
||||
# Database import
|
||||
if db_config.get("import"):
|
||||
imports.append(db_config["import"])
|
||||
|
||||
# Email/Password
|
||||
if "1" in auth_methods:
|
||||
config_parts.append(""" emailAndPassword: {
|
||||
enabled: true,
|
||||
autoSignIn: true
|
||||
}""")
|
||||
|
||||
# OAuth providers
|
||||
social_providers = []
|
||||
if "2" in auth_methods:
|
||||
social_providers.append(""" github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
}""")
|
||||
|
||||
if "3" in auth_methods:
|
||||
social_providers.append(""" google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
}""")
|
||||
|
||||
if "4" in auth_methods:
|
||||
social_providers.append(""" discord: {
|
||||
clientId: process.env.DISCORD_CLIENT_ID!,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
||||
}""")
|
||||
|
||||
if social_providers:
|
||||
config_parts.append(f" socialProviders: {{\n{',\\n'.join(social_providers)}\n }}")
|
||||
|
||||
# Plugins
|
||||
if "5" in auth_methods:
|
||||
plugin_imports.append("import { twoFactor } from 'better-auth/plugins';")
|
||||
plugins.append("twoFactor()")
|
||||
|
||||
if "6" in auth_methods:
|
||||
plugin_imports.append("import { passkey } from 'better-auth/plugins';")
|
||||
plugins.append("passkey()")
|
||||
|
||||
if "7" in auth_methods:
|
||||
plugin_imports.append("import { magicLink } from 'better-auth/plugins';")
|
||||
plugins.append("""magicLink({
|
||||
sendMagicLink: async ({ email, url }) => {
|
||||
// TODO: Implement email sending
|
||||
console.log(`Magic link for ${email}: ${url}`);
|
||||
}
|
||||
})""")
|
||||
|
||||
if "8" in auth_methods:
|
||||
plugin_imports.append("import { username } from 'better-auth/plugins';")
|
||||
plugins.append("username()")
|
||||
|
||||
# Combine all imports
|
||||
all_imports = imports + plugin_imports
|
||||
|
||||
# Build config
|
||||
config_body = ",\n".join(config_parts)
|
||||
|
||||
if plugins:
|
||||
plugins_str = ",\n ".join(plugins)
|
||||
config_body += f",\n plugins: [\n {plugins_str}\n ]"
|
||||
|
||||
# Final output
|
||||
return f"""{chr(10).join(all_imports)}
|
||||
|
||||
export const auth = betterAuth({{
|
||||
{db_config["config"]},
|
||||
{config_body}
|
||||
}});
|
||||
"""
|
||||
|
||||
def generate_env_file(
|
||||
self,
|
||||
db_config: Dict[str, Any],
|
||||
auth_methods: List[str]
|
||||
) -> str:
|
||||
"""
|
||||
Generate .env file content.
|
||||
|
||||
Args:
|
||||
db_config: Database configuration.
|
||||
auth_methods: Selected authentication methods.
|
||||
|
||||
Returns:
|
||||
Generated .env file content.
|
||||
"""
|
||||
env_vars = [
|
||||
f"BETTER_AUTH_SECRET={self.generate_secret()}",
|
||||
"BETTER_AUTH_URL=http://localhost:3000",
|
||||
]
|
||||
|
||||
# Database URL
|
||||
if db_config.get("env_var"):
|
||||
key, value = db_config["env_var"]
|
||||
env_vars.append(f"{key}={value}")
|
||||
|
||||
# OAuth credentials
|
||||
if "2" in auth_methods:
|
||||
env_vars.extend([
|
||||
"GITHUB_CLIENT_ID=your_github_client_id",
|
||||
"GITHUB_CLIENT_SECRET=your_github_client_secret",
|
||||
])
|
||||
|
||||
if "3" in auth_methods:
|
||||
env_vars.extend([
|
||||
"GOOGLE_CLIENT_ID=your_google_client_id",
|
||||
"GOOGLE_CLIENT_SECRET=your_google_client_secret",
|
||||
])
|
||||
|
||||
if "4" in auth_methods:
|
||||
env_vars.extend([
|
||||
"DISCORD_CLIENT_ID=your_discord_client_id",
|
||||
"DISCORD_CLIENT_SECRET=your_discord_client_secret",
|
||||
])
|
||||
|
||||
return "\n".join(env_vars) + "\n"
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run interactive initialization."""
|
||||
print("=" * 50)
|
||||
print("Better Auth Configuration Generator")
|
||||
print("=" * 50)
|
||||
|
||||
# Load existing env
|
||||
env_vars = self._load_env_files()
|
||||
|
||||
# Prompt for configuration
|
||||
db_config = self.prompt_database()
|
||||
auth_methods = self.prompt_auth_methods()
|
||||
|
||||
# Generate files
|
||||
auth_config = self.generate_auth_config(db_config, auth_methods)
|
||||
env_content = self.generate_env_file(db_config, auth_methods)
|
||||
|
||||
# Display output
|
||||
print("\n" + "=" * 50)
|
||||
print("Generated Configuration")
|
||||
print("=" * 50)
|
||||
|
||||
print("\n--- auth.ts ---")
|
||||
print(auth_config)
|
||||
|
||||
print("\n--- .env ---")
|
||||
print(env_content)
|
||||
|
||||
# Offer to save
|
||||
save = input("\nSave configuration files? (y/N): ").strip().lower()
|
||||
if save == "y":
|
||||
self._save_files(auth_config, env_content)
|
||||
else:
|
||||
print("Configuration not saved.")
|
||||
|
||||
def _save_files(self, auth_config: str, env_content: str) -> None:
|
||||
"""
|
||||
Save generated configuration files.
|
||||
|
||||
Args:
|
||||
auth_config: auth.ts content.
|
||||
env_content: .env content.
|
||||
"""
|
||||
# Save auth.ts
|
||||
auth_locations = [
|
||||
self.project_root / "lib" / "auth.ts",
|
||||
self.project_root / "src" / "lib" / "auth.ts",
|
||||
self.project_root / "utils" / "auth.ts",
|
||||
self.project_root / "auth.ts",
|
||||
]
|
||||
|
||||
print("\nWhere to save auth.ts?")
|
||||
for i, loc in enumerate(auth_locations, 1):
|
||||
print(f"{i}. {loc}")
|
||||
print("5. Custom path")
|
||||
|
||||
choice = input("Select (1-5): ").strip()
|
||||
if choice == "5":
|
||||
custom_path = input("Enter path: ").strip()
|
||||
auth_path = Path(custom_path)
|
||||
else:
|
||||
idx = int(choice) - 1 if choice.isdigit() else 0
|
||||
auth_path = auth_locations[idx]
|
||||
|
||||
auth_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
auth_path.write_text(auth_config)
|
||||
print(f"Saved: {auth_path}")
|
||||
|
||||
# Save .env
|
||||
env_path = self.project_root / ".env"
|
||||
if env_path.exists():
|
||||
backup = self.project_root / ".env.backup"
|
||||
env_path.rename(backup)
|
||||
print(f"Backed up existing .env to {backup}")
|
||||
|
||||
env_path.write_text(env_content)
|
||||
print(f"Saved: {env_path}")
|
||||
|
||||
print("\nNext steps:")
|
||||
print("1. Run: npx @better-auth/cli generate")
|
||||
print("2. Apply database migrations")
|
||||
print("3. Mount API handler in your framework")
|
||||
print("4. Create client instance")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Main entry point.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for error).
|
||||
"""
|
||||
try:
|
||||
initializer = BetterAuthInit()
|
||||
initializer.run()
|
||||
return 0
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nOperation cancelled.")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
15
skills/better-auth/scripts/requirements.txt
Normal file
15
skills/better-auth/scripts/requirements.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# Better Auth Skill Dependencies
|
||||
# Python 3.10+ required
|
||||
|
||||
# No Python package dependencies - uses only standard library
|
||||
|
||||
# Testing dependencies (dev)
|
||||
pytest>=8.0.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-mock>=3.12.0
|
||||
|
||||
# Note: This script generates Better Auth configuration
|
||||
# The actual Better Auth library is installed via npm/pnpm/yarn:
|
||||
# npm install better-auth
|
||||
# pnpm add better-auth
|
||||
# yarn add better-auth
|
||||
421
skills/better-auth/scripts/tests/test_better_auth_init.py
Normal file
421
skills/better-auth/scripts/tests/test_better_auth_init.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
Tests for better_auth_init.py
|
||||
|
||||
Covers main functionality with mocked I/O and file operations.
|
||||
Target: >80% coverage
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, mock_open, MagicMock
|
||||
from io import StringIO
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from better_auth_init import BetterAuthInit, EnvConfig, main
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_project_root(tmp_path):
|
||||
"""Create mock project root with package.json."""
|
||||
(tmp_path / "package.json").write_text("{}")
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_init(mock_project_root):
|
||||
"""Create BetterAuthInit instance with mock project root."""
|
||||
return BetterAuthInit(project_root=mock_project_root)
|
||||
|
||||
|
||||
class TestBetterAuthInit:
|
||||
"""Test BetterAuthInit class."""
|
||||
|
||||
def test_init_with_project_root(self, mock_project_root):
|
||||
"""Test initialization with explicit project root."""
|
||||
init = BetterAuthInit(project_root=mock_project_root)
|
||||
assert init.project_root == mock_project_root
|
||||
assert init.env_config is None
|
||||
|
||||
def test_find_project_root_success(self, mock_project_root, monkeypatch):
|
||||
"""Test finding project root successfully."""
|
||||
monkeypatch.chdir(mock_project_root)
|
||||
init = BetterAuthInit()
|
||||
assert init.project_root == mock_project_root
|
||||
|
||||
def test_find_project_root_failure(self, tmp_path, monkeypatch):
|
||||
"""Test failure to find project root."""
|
||||
# Create path without package.json
|
||||
no_package_dir = tmp_path / "no-package"
|
||||
no_package_dir.mkdir()
|
||||
monkeypatch.chdir(no_package_dir)
|
||||
|
||||
# Mock parent to stop infinite loop
|
||||
with patch.object(Path, "parent", new_callable=lambda: property(lambda self: self)):
|
||||
with pytest.raises(RuntimeError, match="Could not find project root"):
|
||||
BetterAuthInit()
|
||||
|
||||
def test_generate_secret(self):
|
||||
"""Test secret generation."""
|
||||
secret = BetterAuthInit.generate_secret()
|
||||
assert len(secret) == 64 # 32 bytes = 64 hex chars
|
||||
assert all(c in "0123456789abcdef" for c in secret)
|
||||
|
||||
# Test custom length
|
||||
secret = BetterAuthInit.generate_secret(length=16)
|
||||
assert len(secret) == 32 # 16 bytes = 32 hex chars
|
||||
|
||||
def test_parse_env_file(self, tmp_path):
|
||||
"""Test parsing .env file."""
|
||||
env_content = """
|
||||
# Comment
|
||||
KEY1=value1
|
||||
KEY2="value2"
|
||||
KEY3='value3'
|
||||
INVALID LINE
|
||||
KEY4=value=with=equals
|
||||
"""
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text(env_content)
|
||||
|
||||
result = BetterAuthInit._parse_env_file(env_file)
|
||||
|
||||
assert result["KEY1"] == "value1"
|
||||
assert result["KEY2"] == "value2"
|
||||
assert result["KEY3"] == "value3"
|
||||
assert result["KEY4"] == "value=with=equals"
|
||||
assert "INVALID" not in result
|
||||
|
||||
def test_parse_env_file_missing(self, tmp_path):
|
||||
"""Test parsing missing .env file."""
|
||||
result = BetterAuthInit._parse_env_file(tmp_path / "nonexistent.env")
|
||||
assert result == {}
|
||||
|
||||
def test_load_env_files(self, auth_init, mock_project_root):
|
||||
"""Test loading environment variables from multiple files."""
|
||||
# Create .env files
|
||||
claude_env = mock_project_root / ".claude" / ".env"
|
||||
claude_env.parent.mkdir(parents=True, exist_ok=True)
|
||||
claude_env.write_text("BASE_VAR=base\nOVERRIDE=claude")
|
||||
|
||||
skills_env = mock_project_root / ".claude" / "skills" / ".env"
|
||||
skills_env.parent.mkdir(parents=True, exist_ok=True)
|
||||
skills_env.write_text("OVERRIDE=skills\nSKILLS_VAR=skills")
|
||||
|
||||
# Mock process env (highest priority)
|
||||
with patch.dict("os.environ", {"OVERRIDE": "process", "PROCESS_VAR": "process"}):
|
||||
result = auth_init._load_env_files()
|
||||
|
||||
assert result["BASE_VAR"] == "base"
|
||||
assert result["SKILLS_VAR"] == "skills"
|
||||
assert result["OVERRIDE"] == "process" # Process env wins
|
||||
assert result["PROCESS_VAR"] == "process"
|
||||
|
||||
def test_prompt_direct_db_sqlite(self, auth_init):
|
||||
"""Test prompting for SQLite database."""
|
||||
with patch("builtins.input", side_effect=["3", "./test.db"]):
|
||||
config = auth_init._prompt_direct_db()
|
||||
|
||||
assert config["type"] == "sqlite"
|
||||
assert "better-sqlite3" in config["import"]
|
||||
assert "./test.db" in config["config"]
|
||||
|
||||
def test_prompt_direct_db_postgresql(self, auth_init):
|
||||
"""Test prompting for PostgreSQL database."""
|
||||
with patch("builtins.input", side_effect=["1", "postgresql://localhost/test"]):
|
||||
config = auth_init._prompt_direct_db()
|
||||
|
||||
assert config["type"] == "postgresql"
|
||||
assert "pg" in config["import"]
|
||||
assert config["env_var"] == ("DATABASE_URL", "postgresql://localhost/test")
|
||||
|
||||
def test_prompt_direct_db_mysql(self, auth_init):
|
||||
"""Test prompting for MySQL database."""
|
||||
with patch("builtins.input", side_effect=["2", "mysql://localhost/test"]):
|
||||
config = auth_init._prompt_direct_db()
|
||||
|
||||
assert config["type"] == "mysql"
|
||||
assert "mysql2" in config["import"]
|
||||
assert config["env_var"][0] == "DATABASE_URL"
|
||||
|
||||
def test_prompt_drizzle(self, auth_init):
|
||||
"""Test prompting for Drizzle ORM."""
|
||||
with patch("builtins.input", return_value="1"):
|
||||
config = auth_init._prompt_drizzle()
|
||||
|
||||
assert config["type"] == "drizzle"
|
||||
assert config["provider"] == "pg"
|
||||
assert "drizzleAdapter" in config["import"]
|
||||
assert "drizzleAdapter" in config["config"]
|
||||
|
||||
def test_prompt_prisma(self, auth_init):
|
||||
"""Test prompting for Prisma."""
|
||||
with patch("builtins.input", return_value="2"):
|
||||
config = auth_init._prompt_prisma()
|
||||
|
||||
assert config["type"] == "prisma"
|
||||
assert config["provider"] == "mysql"
|
||||
assert "prismaAdapter" in config["import"]
|
||||
assert "PrismaClient" in config["import"]
|
||||
|
||||
def test_prompt_kysely(self, auth_init):
|
||||
"""Test prompting for Kysely."""
|
||||
config = auth_init._prompt_kysely()
|
||||
|
||||
assert config["type"] == "kysely"
|
||||
assert "kyselyAdapter" in config["import"]
|
||||
|
||||
def test_prompt_mongodb(self, auth_init):
|
||||
"""Test prompting for MongoDB."""
|
||||
with patch("builtins.input", side_effect=["mongodb://localhost/test", "mydb"]):
|
||||
config = auth_init._prompt_mongodb()
|
||||
|
||||
assert config["type"] == "mongodb"
|
||||
assert "mongodbAdapter" in config["import"]
|
||||
assert "mydb" in config["config"]
|
||||
assert config["env_var"] == ("MONGODB_URI", "mongodb://localhost/test")
|
||||
|
||||
def test_prompt_database(self, auth_init):
|
||||
"""Test database prompting with different choices."""
|
||||
# Test valid choice
|
||||
with patch("builtins.input", side_effect=["3", "1"]):
|
||||
config = auth_init.prompt_database()
|
||||
assert config["type"] == "prisma"
|
||||
|
||||
# Test invalid choice (defaults to direct DB)
|
||||
with patch("builtins.input", side_effect=["99", "1", "postgresql://localhost/test"]):
|
||||
with patch("builtins.print"):
|
||||
config = auth_init.prompt_database()
|
||||
assert config["type"] == "postgresql"
|
||||
|
||||
def test_prompt_auth_methods(self, auth_init):
|
||||
"""Test prompting for authentication methods."""
|
||||
with patch("builtins.input", return_value="1 2 3 5 8"):
|
||||
with patch("builtins.print"):
|
||||
methods = auth_init.prompt_auth_methods()
|
||||
|
||||
assert methods == ["1", "2", "3", "5", "8"]
|
||||
|
||||
def test_prompt_auth_methods_invalid(self, auth_init):
|
||||
"""Test filtering invalid auth method choices."""
|
||||
with patch("builtins.input", return_value="1 99 abc 3"):
|
||||
with patch("builtins.print"):
|
||||
methods = auth_init.prompt_auth_methods()
|
||||
|
||||
assert methods == ["1", "3"]
|
||||
|
||||
def test_generate_auth_config_basic(self, auth_init):
|
||||
"""Test generating basic auth config."""
|
||||
db_config = {
|
||||
"import": "import Database from 'better-sqlite3';",
|
||||
"config": "database: new Database('./dev.db')"
|
||||
}
|
||||
auth_methods = ["1"] # Email/password only
|
||||
|
||||
config = auth_init.generate_auth_config(db_config, auth_methods)
|
||||
|
||||
assert "import { betterAuth }" in config
|
||||
assert "emailAndPassword" in config
|
||||
assert "enabled: true" in config
|
||||
assert "better-sqlite3" in config
|
||||
|
||||
def test_generate_auth_config_with_oauth(self, auth_init):
|
||||
"""Test generating config with OAuth providers."""
|
||||
db_config = {
|
||||
"import": "import { Pool } from 'pg';",
|
||||
"config": "database: new Pool()"
|
||||
}
|
||||
auth_methods = ["1", "2", "3", "4"] # Email + GitHub + Google + Discord
|
||||
|
||||
config = auth_init.generate_auth_config(db_config, auth_methods)
|
||||
|
||||
assert "socialProviders" in config
|
||||
assert "github:" in config
|
||||
assert "google:" in config
|
||||
assert "discord:" in config
|
||||
assert "GITHUB_CLIENT_ID" in config
|
||||
assert "GOOGLE_CLIENT_ID" in config
|
||||
assert "DISCORD_CLIENT_ID" in config
|
||||
|
||||
def test_generate_auth_config_with_plugins(self, auth_init):
|
||||
"""Test generating config with plugins."""
|
||||
db_config = {"import": "", "config": "database: db"}
|
||||
auth_methods = ["5", "6", "7", "8"] # 2FA, Passkey, Magic Link, Username
|
||||
|
||||
config = auth_init.generate_auth_config(db_config, auth_methods)
|
||||
|
||||
assert "plugins:" in config
|
||||
assert "twoFactor" in config
|
||||
assert "passkey" in config
|
||||
assert "magicLink" in config
|
||||
assert "username" in config
|
||||
assert "from 'better-auth/plugins'" in config
|
||||
|
||||
def test_generate_env_file_basic(self, auth_init):
|
||||
"""Test generating basic .env file."""
|
||||
db_config = {"type": "sqlite"}
|
||||
auth_methods = ["1"]
|
||||
|
||||
env_content = auth_init.generate_env_file(db_config, auth_methods)
|
||||
|
||||
assert "BETTER_AUTH_SECRET=" in env_content
|
||||
assert "BETTER_AUTH_URL=http://localhost:3000" in env_content
|
||||
assert len(env_content.split("\n")) >= 2
|
||||
|
||||
def test_generate_env_file_with_database_url(self, auth_init):
|
||||
"""Test generating .env with database URL."""
|
||||
db_config = {
|
||||
"env_var": ("DATABASE_URL", "postgresql://localhost/test")
|
||||
}
|
||||
auth_methods = []
|
||||
|
||||
env_content = auth_init.generate_env_file(db_config, auth_methods)
|
||||
|
||||
assert "DATABASE_URL=postgresql://localhost/test" in env_content
|
||||
|
||||
def test_generate_env_file_with_oauth(self, auth_init):
|
||||
"""Test generating .env with OAuth credentials."""
|
||||
db_config = {}
|
||||
auth_methods = ["2", "3", "4"] # GitHub, Google, Discord
|
||||
|
||||
env_content = auth_init.generate_env_file(db_config, auth_methods)
|
||||
|
||||
assert "GITHUB_CLIENT_ID=" in env_content
|
||||
assert "GITHUB_CLIENT_SECRET=" in env_content
|
||||
assert "GOOGLE_CLIENT_ID=" in env_content
|
||||
assert "GOOGLE_CLIENT_SECRET=" in env_content
|
||||
assert "DISCORD_CLIENT_ID=" in env_content
|
||||
assert "DISCORD_CLIENT_SECRET=" in env_content
|
||||
|
||||
def test_save_files(self, auth_init, mock_project_root):
|
||||
"""Test saving configuration files."""
|
||||
auth_config = "// auth config"
|
||||
env_content = "SECRET=test"
|
||||
|
||||
with patch("builtins.input", side_effect=["1"]):
|
||||
auth_init._save_files(auth_config, env_content)
|
||||
|
||||
# Check auth.ts was saved
|
||||
auth_path = mock_project_root / "lib" / "auth.ts"
|
||||
assert auth_path.exists()
|
||||
assert auth_path.read_text() == auth_config
|
||||
|
||||
# Check .env was saved
|
||||
env_path = mock_project_root / ".env"
|
||||
assert env_path.exists()
|
||||
assert env_path.read_text() == env_content
|
||||
|
||||
def test_save_files_custom_path(self, auth_init, mock_project_root):
|
||||
"""Test saving with custom path."""
|
||||
auth_config = "// config"
|
||||
env_content = "SECRET=test"
|
||||
|
||||
custom_path = str(mock_project_root / "custom" / "auth.ts")
|
||||
with patch("builtins.input", side_effect=["5", custom_path]):
|
||||
auth_init._save_files(auth_config, env_content)
|
||||
|
||||
assert Path(custom_path).exists()
|
||||
|
||||
def test_save_files_backup_existing_env(self, auth_init, mock_project_root):
|
||||
"""Test backing up existing .env file."""
|
||||
# Create existing .env
|
||||
env_path = mock_project_root / ".env"
|
||||
env_path.write_text("OLD_SECRET=old")
|
||||
|
||||
auth_config = "// config"
|
||||
env_content = "NEW_SECRET=new"
|
||||
|
||||
with patch("builtins.input", return_value="1"):
|
||||
auth_init._save_files(auth_config, env_content)
|
||||
|
||||
# Check backup was created
|
||||
backup_path = mock_project_root / ".env.backup"
|
||||
assert backup_path.exists()
|
||||
assert backup_path.read_text() == "OLD_SECRET=old"
|
||||
|
||||
# Check new .env
|
||||
assert env_path.read_text() == "NEW_SECRET=new"
|
||||
|
||||
def test_run_full_flow(self, auth_init, mock_project_root):
|
||||
"""Test complete run flow."""
|
||||
inputs = [
|
||||
"1", # Direct DB
|
||||
"1", # PostgreSQL
|
||||
"postgresql://localhost/test",
|
||||
"1 2", # Email + GitHub
|
||||
"n" # Don't save
|
||||
]
|
||||
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
with patch("builtins.print"):
|
||||
auth_init.run()
|
||||
|
||||
# Should complete without errors
|
||||
# Files not saved because user chose 'n'
|
||||
assert not (mock_project_root / "auth.ts").exists()
|
||||
|
||||
def test_run_save_files(self, auth_init, mock_project_root):
|
||||
"""Test run flow with file saving."""
|
||||
inputs = [
|
||||
"1", # Direct DB
|
||||
"3", # SQLite
|
||||
"", # Default path
|
||||
"1", # Email only
|
||||
"y", # Save
|
||||
"1" # Save location
|
||||
]
|
||||
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
with patch("builtins.print"):
|
||||
auth_init.run()
|
||||
|
||||
# Check files were created
|
||||
assert (mock_project_root / "lib" / "auth.ts").exists()
|
||||
assert (mock_project_root / ".env").exists()
|
||||
|
||||
|
||||
class TestMainFunction:
|
||||
"""Test main entry point."""
|
||||
|
||||
def test_main_success(self, tmp_path, monkeypatch):
|
||||
"""Test successful main execution."""
|
||||
(tmp_path / "package.json").write_text("{}")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
inputs = ["1", "3", "", "1", "n"]
|
||||
|
||||
with patch("builtins.input", side_effect=inputs):
|
||||
with patch("builtins.print"):
|
||||
exit_code = main()
|
||||
|
||||
assert exit_code == 0
|
||||
|
||||
def test_main_keyboard_interrupt(self, tmp_path, monkeypatch):
|
||||
"""Test main with keyboard interrupt."""
|
||||
(tmp_path / "package.json").write_text("{}")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch("builtins.input", side_effect=KeyboardInterrupt()):
|
||||
with patch("builtins.print"):
|
||||
exit_code = main()
|
||||
|
||||
assert exit_code == 1
|
||||
|
||||
def test_main_error(self, tmp_path, monkeypatch):
|
||||
"""Test main with error."""
|
||||
# No package.json - should fail
|
||||
no_package = tmp_path / "no-package"
|
||||
no_package.mkdir()
|
||||
monkeypatch.chdir(no_package)
|
||||
|
||||
with patch.object(Path, "parent", new_callable=lambda: property(lambda self: self)):
|
||||
with patch("sys.stderr", new_callable=StringIO):
|
||||
exit_code = main()
|
||||
|
||||
assert exit_code == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "--cov=better_auth_init", "--cov-report=term-missing"])
|
||||
360
skills/chrome-devtools/SKILL.md
Normal file
360
skills/chrome-devtools/SKILL.md
Normal file
@@ -0,0 +1,360 @@
|
||||
---
|
||||
name: chrome-devtools
|
||||
description: Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging.
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Chrome DevTools Agent Skill
|
||||
|
||||
Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**CRITICAL**: Always check `pwd` before running scripts.
|
||||
|
||||
### Installation
|
||||
|
||||
#### Step 1: Install System Dependencies (Linux/WSL only)
|
||||
|
||||
On Linux/WSL, Chrome requires system libraries. Install them first:
|
||||
|
||||
```bash
|
||||
pwd # Should show current working directory
|
||||
cd .claude/skills/chrome-devtools/scripts
|
||||
./install-deps.sh # Auto-detects OS and installs required libs
|
||||
```
|
||||
|
||||
Supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro
|
||||
|
||||
**macOS/Windows**: Skip this step (dependencies bundled with Chrome)
|
||||
|
||||
#### Step 2: Install Node Dependencies
|
||||
|
||||
```bash
|
||||
npm install # Installs puppeteer, debug, yargs
|
||||
```
|
||||
|
||||
#### Step 3: Install ImageMagick (Optional, Recommended)
|
||||
|
||||
ImageMagick enables automatic screenshot compression to keep files under 5MB:
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install imagemagick
|
||||
```
|
||||
|
||||
**Ubuntu/Debian/WSL:**
|
||||
```bash
|
||||
sudo apt-get install imagemagick
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
magick -version # or: convert -version
|
||||
```
|
||||
|
||||
Without ImageMagick, screenshots >5MB will not be compressed (may fail to load in Gemini/Claude).
|
||||
|
||||
### Test
|
||||
```bash
|
||||
node navigate.js --url https://example.com
|
||||
# Output: {"success": true, "url": "https://example.com", "title": "Example Domain"}
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
All scripts are in `.claude/skills/chrome-devtools/scripts/`
|
||||
|
||||
**CRITICAL**: Always check `pwd` before running scripts.
|
||||
|
||||
### Script Usage
|
||||
- `./scripts/README.md`
|
||||
|
||||
### Core Automation
|
||||
- `navigate.js` - Navigate to URLs
|
||||
- `screenshot.js` - Capture screenshots (full page or element)
|
||||
- `click.js` - Click elements
|
||||
- `fill.js` - Fill form fields
|
||||
- `evaluate.js` - Execute JavaScript in page context
|
||||
|
||||
### Analysis & Monitoring
|
||||
- `snapshot.js` - Extract interactive elements with metadata
|
||||
- `console.js` - Monitor console messages/errors
|
||||
- `network.js` - Track HTTP requests/responses
|
||||
- `performance.js` - Measure Core Web Vitals + record traces
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Single Command
|
||||
```bash
|
||||
pwd # Should show current working directory
|
||||
cd .claude/skills/chrome-devtools/scripts
|
||||
node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
|
||||
```
|
||||
**Important**: Always save screenshots to `./docs/screenshots` directory.
|
||||
|
||||
### Automatic Image Compression
|
||||
Screenshots are **automatically compressed** if they exceed 5MB to ensure compatibility with Gemini API and Claude Code (which have 5MB limits). This uses ImageMagick internally:
|
||||
|
||||
```bash
|
||||
# Default: auto-compress if >5MB
|
||||
node screenshot.js --url https://example.com --output page.png
|
||||
|
||||
# Custom size threshold (e.g., 3MB)
|
||||
node screenshot.js --url https://example.com --output page.png --max-size 3
|
||||
|
||||
# Disable compression
|
||||
node screenshot.js --url https://example.com --output page.png --no-compress
|
||||
```
|
||||
|
||||
**Compression behavior:**
|
||||
- PNG: Resizes to 90% + quality 85 (or 75% + quality 70 if still too large)
|
||||
- JPEG: Quality 80 + progressive encoding (or quality 60 if still too large)
|
||||
- Other formats: Converted to JPEG with compression
|
||||
- Requires ImageMagick installed (see imagemagick skill)
|
||||
|
||||
**Output includes compression info:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output": "/path/to/page.png",
|
||||
"compressed": true,
|
||||
"originalSize": 8388608,
|
||||
"size": 3145728,
|
||||
"compressionRatio": "62.50%",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Chain Commands (reuse browser)
|
||||
```bash
|
||||
# Keep browser open with --close false
|
||||
node navigate.js --url https://example.com/login --close false
|
||||
node fill.js --selector "#email" --value "user@example.com" --close false
|
||||
node fill.js --selector "#password" --value "secret" --close false
|
||||
node click.js --selector "button[type=submit]"
|
||||
```
|
||||
|
||||
### Parse JSON Output
|
||||
```bash
|
||||
# Extract specific fields with jq
|
||||
node performance.js --url https://example.com | jq '.vitals.LCP'
|
||||
|
||||
# Save to file
|
||||
node network.js --url https://example.com --output /tmp/requests.json
|
||||
```
|
||||
|
||||
## Execution Protocol
|
||||
|
||||
### Working Directory Verification
|
||||
|
||||
BEFORE executing any script:
|
||||
1. Check current working directory with `pwd`
|
||||
2. Verify in `.claude/skills/chrome-devtools/scripts/` directory
|
||||
3. If wrong directory, `cd` to correct location
|
||||
4. Use absolute paths for all output files
|
||||
|
||||
Example:
|
||||
```bash
|
||||
pwd # Should show: .../chrome-devtools/scripts
|
||||
# If wrong:
|
||||
cd .claude/skills/chrome-devtools/scripts
|
||||
```
|
||||
|
||||
### Output Validation
|
||||
|
||||
AFTER screenshot/capture operations:
|
||||
1. Verify file created with `ls -lh <output-path>`
|
||||
2. Read screenshot using Read tool to confirm content
|
||||
3. Check JSON output for success:true
|
||||
4. Report file size and compression status
|
||||
|
||||
Example:
|
||||
```bash
|
||||
node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
|
||||
ls -lh ./docs/screenshots/page.png # Verify file exists
|
||||
# Then use Read tool to visually inspect
|
||||
```
|
||||
|
||||
5. Restart working directory to the project root.
|
||||
|
||||
### Error Recovery
|
||||
|
||||
If script fails:
|
||||
1. Check error message for selector issues
|
||||
2. Use snapshot.js to discover correct selectors
|
||||
3. Try XPath selector if CSS selector fails
|
||||
4. Verify element is visible and interactive
|
||||
|
||||
Example:
|
||||
```bash
|
||||
# CSS selector fails
|
||||
node click.js --url https://example.com --selector ".btn-submit"
|
||||
# Error: waiting for selector ".btn-submit" failed
|
||||
|
||||
# Discover correct selector
|
||||
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
|
||||
|
||||
# Try XPath
|
||||
node click.js --url https://example.com --selector "//button[contains(text(),'Submit')]"
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
❌ Wrong working directory → output files go to wrong location
|
||||
❌ Skipping output validation → silent failures
|
||||
❌ Using complex CSS selectors without testing → selector errors
|
||||
❌ Not checking element visibility → timeout errors
|
||||
|
||||
✅ Always verify `pwd` before running scripts
|
||||
✅ Always validate output after screenshots
|
||||
✅ Use snapshot.js to discover selectors
|
||||
✅ Test selectors with simple commands first
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Web Scraping
|
||||
```bash
|
||||
node evaluate.js --url https://example.com --script "
|
||||
Array.from(document.querySelectorAll('.item')).map(el => ({
|
||||
title: el.querySelector('h2')?.textContent,
|
||||
link: el.querySelector('a')?.href
|
||||
}))
|
||||
" | jq '.result'
|
||||
```
|
||||
|
||||
### Performance Testing
|
||||
```bash
|
||||
PERF=$(node performance.js --url https://example.com)
|
||||
LCP=$(echo $PERF | jq '.vitals.LCP')
|
||||
if (( $(echo "$LCP < 2500" | bc -l) )); then
|
||||
echo "✓ LCP passed: ${LCP}ms"
|
||||
else
|
||||
echo "✗ LCP failed: ${LCP}ms"
|
||||
fi
|
||||
```
|
||||
|
||||
### Form Automation
|
||||
```bash
|
||||
node fill.js --url https://example.com --selector "#search" --value "query" --close false
|
||||
node click.js --selector "button[type=submit]"
|
||||
```
|
||||
|
||||
### Error Monitoring
|
||||
```bash
|
||||
node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount'
|
||||
```
|
||||
|
||||
## Script Options
|
||||
|
||||
All scripts support:
|
||||
- `--headless false` - Show browser window
|
||||
- `--close false` - Keep browser open for chaining
|
||||
- `--timeout 30000` - Set timeout (milliseconds)
|
||||
- `--wait-until networkidle2` - Wait strategy
|
||||
|
||||
See `./scripts/README.md` for complete options.
|
||||
|
||||
## Output Format
|
||||
|
||||
All scripts output JSON to stdout:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"url": "https://example.com",
|
||||
... // script-specific data
|
||||
}
|
||||
```
|
||||
|
||||
Errors go to stderr:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
## Finding Elements
|
||||
|
||||
Use `snapshot.js` to discover selectors:
|
||||
```bash
|
||||
node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
**"Cannot find package 'puppeteer'"**
|
||||
- Run: `npm install` in the scripts directory
|
||||
|
||||
**"error while loading shared libraries: libnss3.so"** (Linux/WSL)
|
||||
- Missing system dependencies
|
||||
- Fix: Run `./install-deps.sh` in scripts directory
|
||||
- Manual install: `sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1`
|
||||
|
||||
**"Failed to launch the browser process"**
|
||||
- Check system dependencies installed (Linux/WSL)
|
||||
- Verify Chrome downloaded: `ls ~/.cache/puppeteer`
|
||||
- Try: `npm rebuild` then `npm install`
|
||||
|
||||
**Chrome not found**
|
||||
- Puppeteer auto-downloads Chrome during `npm install`
|
||||
- If failed, manually trigger: `npx puppeteer browsers install chrome`
|
||||
|
||||
### Script Issues
|
||||
|
||||
**Element not found**
|
||||
- Get snapshot first to find correct selector: `node snapshot.js --url <url>`
|
||||
|
||||
**Script hangs**
|
||||
- Increase timeout: `--timeout 60000`
|
||||
- Change wait strategy: `--wait-until load` or `--wait-until domcontentloaded`
|
||||
|
||||
**Blank screenshot**
|
||||
- Wait for page load: `--wait-until networkidle2`
|
||||
- Increase timeout: `--timeout 30000`
|
||||
|
||||
**Permission denied on scripts**
|
||||
- Make executable: `chmod +x *.sh`
|
||||
|
||||
**Screenshot too large (>5MB)**
|
||||
- Install ImageMagick for automatic compression
|
||||
- Manually set lower threshold: `--max-size 3`
|
||||
- Use JPEG format instead of PNG: `--format jpeg --quality 80`
|
||||
- Capture specific element instead of full page: `--selector .main-content`
|
||||
|
||||
**Compression not working**
|
||||
- Verify ImageMagick installed: `magick -version` or `convert -version`
|
||||
- Check file was actually compressed in output JSON: `"compressed": true`
|
||||
- For very large pages, use `--selector` to capture only needed area
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
Detailed guides available in `./references/`:
|
||||
- [CDP Domains Reference](./references/cdp-domains.md) - 47 Chrome DevTools Protocol domains
|
||||
- [Puppeteer Quick Reference](./references/puppeteer-reference.md) - Complete Puppeteer API patterns
|
||||
- [Performance Analysis Guide](./references/performance-guide.md) - Core Web Vitals optimization
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Scripts
|
||||
Create custom scripts using shared library:
|
||||
```javascript
|
||||
import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js';
|
||||
// Your automation logic
|
||||
```
|
||||
|
||||
### Direct CDP Access
|
||||
```javascript
|
||||
const client = await page.createCDPSession();
|
||||
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
|
||||
```
|
||||
|
||||
See reference documentation for advanced patterns and complete API coverage.
|
||||
|
||||
## External Resources
|
||||
|
||||
- [Puppeteer Documentation](https://pptr.dev/)
|
||||
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
|
||||
- [Scripts README](./scripts/README.md)
|
||||
694
skills/chrome-devtools/references/cdp-domains.md
Normal file
694
skills/chrome-devtools/references/cdp-domains.md
Normal file
@@ -0,0 +1,694 @@
|
||||
# Chrome DevTools Protocol (CDP) Domains Reference
|
||||
|
||||
Complete reference of CDP domains and their capabilities for browser automation and debugging.
|
||||
|
||||
## Overview
|
||||
|
||||
CDP is organized into **47 domains**, each providing specific browser capabilities. Domains are grouped by functionality:
|
||||
|
||||
- **Core** - Fundamental browser control
|
||||
- **DOM & Styling** - Page structure and styling
|
||||
- **Network & Fetch** - HTTP traffic management
|
||||
- **Page & Navigation** - Page lifecycle control
|
||||
- **Storage & Data** - Browser storage APIs
|
||||
- **Performance & Profiling** - Metrics and analysis
|
||||
- **Emulation & Simulation** - Device and network emulation
|
||||
- **Worker & Service** - Background tasks
|
||||
- **Developer Tools** - Debugging support
|
||||
|
||||
---
|
||||
|
||||
## Core Domains
|
||||
|
||||
### Runtime
|
||||
**Purpose:** Execute JavaScript, manage objects, handle promises
|
||||
|
||||
**Key Commands:**
|
||||
- `Runtime.evaluate(expression)` - Execute JavaScript
|
||||
- `Runtime.callFunctionOn(functionDeclaration, objectId)` - Call function on object
|
||||
- `Runtime.getProperties(objectId)` - Get object properties
|
||||
- `Runtime.awaitPromise(promiseObjectId)` - Wait for promise resolution
|
||||
|
||||
**Key Events:**
|
||||
- `Runtime.consoleAPICalled` - Console message logged
|
||||
- `Runtime.exceptionThrown` - Uncaught exception
|
||||
|
||||
**Use Cases:**
|
||||
- Execute custom JavaScript
|
||||
- Access page data
|
||||
- Monitor console output
|
||||
- Handle exceptions
|
||||
|
||||
---
|
||||
|
||||
### Debugger
|
||||
**Purpose:** JavaScript debugging, breakpoints, stack traces
|
||||
|
||||
**Key Commands:**
|
||||
- `Debugger.enable()` - Enable debugger
|
||||
- `Debugger.setBreakpoint(location)` - Set breakpoint
|
||||
- `Debugger.pause()` - Pause execution
|
||||
- `Debugger.resume()` - Resume execution
|
||||
- `Debugger.stepOver/stepInto/stepOut()` - Step through code
|
||||
|
||||
**Key Events:**
|
||||
- `Debugger.paused` - Execution paused
|
||||
- `Debugger.resumed` - Execution resumed
|
||||
- `Debugger.scriptParsed` - Script loaded
|
||||
|
||||
**Use Cases:**
|
||||
- Debug JavaScript errors
|
||||
- Inspect call stacks
|
||||
- Set conditional breakpoints
|
||||
- Source map support
|
||||
|
||||
---
|
||||
|
||||
### Console (Deprecated - Use Runtime/Log)
|
||||
**Purpose:** Legacy console message access
|
||||
|
||||
**Note:** Use `Runtime.consoleAPICalled` event instead for new implementations.
|
||||
|
||||
---
|
||||
|
||||
## DOM & Styling Domains
|
||||
|
||||
### DOM
|
||||
**Purpose:** Access and manipulate DOM tree
|
||||
|
||||
**Key Commands:**
|
||||
- `DOM.getDocument()` - Get root document node
|
||||
- `DOM.querySelector(nodeId, selector)` - Query selector
|
||||
- `DOM.querySelectorAll(nodeId, selector)` - Query all
|
||||
- `DOM.getAttributes(nodeId)` - Get element attributes
|
||||
- `DOM.setOuterHTML(nodeId, outerHTML)` - Replace element
|
||||
- `DOM.getBoxModel(nodeId)` - Get element layout box
|
||||
- `DOM.focus(nodeId)` - Focus element
|
||||
|
||||
**Key Events:**
|
||||
- `DOM.documentUpdated` - Document changed
|
||||
- `DOM.setChildNodes` - Child nodes updated
|
||||
|
||||
**Use Cases:**
|
||||
- Navigate DOM tree
|
||||
- Query elements
|
||||
- Modify DOM structure
|
||||
- Get element positions
|
||||
|
||||
---
|
||||
|
||||
### CSS
|
||||
**Purpose:** Inspect and modify CSS styles
|
||||
|
||||
**Key Commands:**
|
||||
- `CSS.enable()` - Enable CSS domain
|
||||
- `CSS.getComputedStyleForNode(nodeId)` - Get computed styles
|
||||
- `CSS.getInlineStylesForNode(nodeId)` - Get inline styles
|
||||
- `CSS.getMatchedStylesForNode(nodeId)` - Get matched CSS rules
|
||||
- `CSS.setStyleTexts(edits)` - Modify styles
|
||||
|
||||
**Key Events:**
|
||||
- `CSS.styleSheetAdded` - Stylesheet added
|
||||
- `CSS.styleSheetChanged` - Stylesheet modified
|
||||
|
||||
**Use Cases:**
|
||||
- Inspect element styles
|
||||
- Debug CSS issues
|
||||
- Modify styles dynamically
|
||||
- Extract stylesheet data
|
||||
|
||||
---
|
||||
|
||||
### Accessibility
|
||||
**Purpose:** Access accessibility tree
|
||||
|
||||
**Key Commands:**
|
||||
- `Accessibility.enable()` - Enable accessibility
|
||||
- `Accessibility.getFullAXTree()` - Get complete AX tree
|
||||
- `Accessibility.getPartialAXTree(nodeId)` - Get node subtree
|
||||
- `Accessibility.queryAXTree(nodeId, role, name)` - Query AX tree
|
||||
|
||||
**Use Cases:**
|
||||
- Accessibility testing
|
||||
- Screen reader simulation
|
||||
- ARIA attribute inspection
|
||||
- AX tree analysis
|
||||
|
||||
---
|
||||
|
||||
## Network & Fetch Domains
|
||||
|
||||
### Network
|
||||
**Purpose:** Monitor and control HTTP traffic
|
||||
|
||||
**Key Commands:**
|
||||
- `Network.enable()` - Enable network tracking
|
||||
- `Network.setCacheDisabled(cacheDisabled)` - Disable cache
|
||||
- `Network.setExtraHTTPHeaders(headers)` - Add custom headers
|
||||
- `Network.getCookies(urls)` - Get cookies
|
||||
- `Network.setCookie(name, value, domain)` - Set cookie
|
||||
- `Network.getResponseBody(requestId)` - Get response body
|
||||
- `Network.emulateNetworkConditions(offline, latency, downloadThroughput, uploadThroughput)` - Throttle network
|
||||
|
||||
**Key Events:**
|
||||
- `Network.requestWillBeSent` - Request starting
|
||||
- `Network.responseReceived` - Response received
|
||||
- `Network.loadingFinished` - Request completed
|
||||
- `Network.loadingFailed` - Request failed
|
||||
|
||||
**Use Cases:**
|
||||
- Monitor API calls
|
||||
- Intercept requests
|
||||
- Analyze response data
|
||||
- Simulate slow networks
|
||||
- Manage cookies
|
||||
|
||||
---
|
||||
|
||||
### Fetch
|
||||
**Purpose:** Intercept and modify network requests
|
||||
|
||||
**Key Commands:**
|
||||
- `Fetch.enable(patterns)` - Enable request interception
|
||||
- `Fetch.continueRequest(requestId, url, method, headers)` - Continue/modify request
|
||||
- `Fetch.fulfillRequest(requestId, responseCode, headers, body)` - Mock response
|
||||
- `Fetch.failRequest(requestId, errorReason)` - Fail request
|
||||
|
||||
**Key Events:**
|
||||
- `Fetch.requestPaused` - Request intercepted
|
||||
|
||||
**Use Cases:**
|
||||
- Mock API responses
|
||||
- Block requests
|
||||
- Modify request/response
|
||||
- Test error scenarios
|
||||
|
||||
---
|
||||
|
||||
## Page & Navigation Domains
|
||||
|
||||
### Page
|
||||
**Purpose:** Control page lifecycle and navigation
|
||||
|
||||
**Key Commands:**
|
||||
- `Page.enable()` - Enable page domain
|
||||
- `Page.navigate(url)` - Navigate to URL
|
||||
- `Page.reload(ignoreCache)` - Reload page
|
||||
- `Page.goBack()/goForward()` - Navigate history
|
||||
- `Page.captureScreenshot(format, quality)` - Take screenshot
|
||||
- `Page.printToPDF(landscape, displayHeaderFooter)` - Generate PDF
|
||||
- `Page.getLayoutMetrics()` - Get page dimensions
|
||||
- `Page.createIsolatedWorld(frameId)` - Create isolated context
|
||||
- `Page.handleJavaScriptDialog(accept, promptText)` - Handle alerts/confirms
|
||||
|
||||
**Key Events:**
|
||||
- `Page.loadEventFired` - Page loaded
|
||||
- `Page.domContentEventFired` - DOM ready
|
||||
- `Page.frameNavigated` - Frame navigated
|
||||
- `Page.javascriptDialogOpening` - Alert/confirm shown
|
||||
|
||||
**Use Cases:**
|
||||
- Navigate pages
|
||||
- Capture screenshots
|
||||
- Generate PDFs
|
||||
- Handle popups
|
||||
- Monitor page lifecycle
|
||||
|
||||
---
|
||||
|
||||
### Target
|
||||
**Purpose:** Manage browser targets (tabs, workers, frames)
|
||||
|
||||
**Key Commands:**
|
||||
- `Target.getTargets()` - List all targets
|
||||
- `Target.createTarget(url)` - Open new tab
|
||||
- `Target.closeTarget(targetId)` - Close tab
|
||||
- `Target.attachToTarget(targetId)` - Attach debugger
|
||||
- `Target.detachFromTarget(sessionId)` - Detach debugger
|
||||
- `Target.setDiscoverTargets(discover)` - Auto-discover targets
|
||||
|
||||
**Key Events:**
|
||||
- `Target.targetCreated` - New target created
|
||||
- `Target.targetDestroyed` - Target closed
|
||||
- `Target.targetInfoChanged` - Target updated
|
||||
|
||||
**Use Cases:**
|
||||
- Multi-tab automation
|
||||
- Service worker debugging
|
||||
- Frame inspection
|
||||
- Extension debugging
|
||||
|
||||
---
|
||||
|
||||
### Input
|
||||
**Purpose:** Simulate user input
|
||||
|
||||
**Key Commands:**
|
||||
- `Input.dispatchKeyEvent(type, key, code)` - Keyboard input
|
||||
- `Input.dispatchMouseEvent(type, x, y, button)` - Mouse input
|
||||
- `Input.dispatchTouchEvent(type, touchPoints)` - Touch input
|
||||
- `Input.synthesizePinchGesture(x, y, scaleFactor)` - Pinch gesture
|
||||
- `Input.synthesizeScrollGesture(x, y, xDistance, yDistance)` - Scroll
|
||||
|
||||
**Use Cases:**
|
||||
- Simulate clicks
|
||||
- Type text
|
||||
- Drag and drop
|
||||
- Touch gestures
|
||||
- Scroll pages
|
||||
|
||||
---
|
||||
|
||||
## Storage & Data Domains
|
||||
|
||||
### Storage
|
||||
**Purpose:** Manage browser storage
|
||||
|
||||
**Key Commands:**
|
||||
- `Storage.getCookies(browserContextId)` - Get cookies
|
||||
- `Storage.setCookies(cookies)` - Set cookies
|
||||
- `Storage.clearCookies(browserContextId)` - Clear cookies
|
||||
- `Storage.clearDataForOrigin(origin, storageTypes)` - Clear storage
|
||||
- `Storage.getUsageAndQuota(origin)` - Get storage usage
|
||||
|
||||
**Storage Types:**
|
||||
- appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage
|
||||
|
||||
**Use Cases:**
|
||||
- Cookie management
|
||||
- Clear browser data
|
||||
- Inspect storage usage
|
||||
- Test quota limits
|
||||
|
||||
---
|
||||
|
||||
### DOMStorage
|
||||
**Purpose:** Access localStorage/sessionStorage
|
||||
|
||||
**Key Commands:**
|
||||
- `DOMStorage.enable()` - Enable storage tracking
|
||||
- `DOMStorage.getDOMStorageItems(storageId)` - Get items
|
||||
- `DOMStorage.setDOMStorageItem(storageId, key, value)` - Set item
|
||||
- `DOMStorage.removeDOMStorageItem(storageId, key)` - Remove item
|
||||
|
||||
**Key Events:**
|
||||
- `DOMStorage.domStorageItemsCleared` - Storage cleared
|
||||
- `DOMStorage.domStorageItemAdded/Updated/Removed` - Item changed
|
||||
|
||||
---
|
||||
|
||||
### IndexedDB
|
||||
**Purpose:** Query IndexedDB databases
|
||||
|
||||
**Key Commands:**
|
||||
- `IndexedDB.requestDatabaseNames(securityOrigin)` - List databases
|
||||
- `IndexedDB.requestDatabase(securityOrigin, databaseName)` - Get DB structure
|
||||
- `IndexedDB.requestData(securityOrigin, databaseName, objectStoreName)` - Query data
|
||||
|
||||
**Use Cases:**
|
||||
- Inspect IndexedDB data
|
||||
- Debug database issues
|
||||
- Extract stored data
|
||||
|
||||
---
|
||||
|
||||
### CacheStorage
|
||||
**Purpose:** Manage Cache API
|
||||
|
||||
**Key Commands:**
|
||||
- `CacheStorage.requestCacheNames(securityOrigin)` - List caches
|
||||
- `CacheStorage.requestCachedResponses(cacheId, securityOrigin)` - List cached responses
|
||||
- `CacheStorage.deleteCache(cacheId)` - Delete cache
|
||||
|
||||
**Use Cases:**
|
||||
- Service worker cache inspection
|
||||
- Offline functionality testing
|
||||
|
||||
---
|
||||
|
||||
## Performance & Profiling Domains
|
||||
|
||||
### Performance
|
||||
**Purpose:** Collect performance metrics
|
||||
|
||||
**Key Commands:**
|
||||
- `Performance.enable()` - Enable performance tracking
|
||||
- `Performance.disable()` - Disable tracking
|
||||
- `Performance.getMetrics()` - Get current metrics
|
||||
|
||||
**Metrics:**
|
||||
- Timestamp, Documents, Frames, JSEventListeners, Nodes, LayoutCount, RecalcStyleCount, LayoutDuration, RecalcStyleDuration, ScriptDuration, TaskDuration, JSHeapUsedSize, JSHeapTotalSize
|
||||
|
||||
**Use Cases:**
|
||||
- Monitor page metrics
|
||||
- Track memory usage
|
||||
- Measure render times
|
||||
|
||||
---
|
||||
|
||||
### PerformanceTimeline
|
||||
**Purpose:** Access Performance Timeline API
|
||||
|
||||
**Key Commands:**
|
||||
- `PerformanceTimeline.enable(eventTypes)` - Subscribe to events
|
||||
|
||||
**Event Types:**
|
||||
- mark, measure, navigation, resource, longtask, paint, layout-shift
|
||||
|
||||
**Key Events:**
|
||||
- `PerformanceTimeline.timelineEventAdded` - New performance entry
|
||||
|
||||
---
|
||||
|
||||
### Tracing
|
||||
**Purpose:** Record Chrome trace
|
||||
|
||||
**Key Commands:**
|
||||
- `Tracing.start(categories, options)` - Start recording
|
||||
- `Tracing.end()` - Stop recording
|
||||
- `Tracing.requestMemoryDump()` - Capture memory snapshot
|
||||
|
||||
**Trace Categories:**
|
||||
- blink, cc, devtools, gpu, loading, navigation, rendering, v8, disabled-by-default-*
|
||||
|
||||
**Key Events:**
|
||||
- `Tracing.dataCollected` - Trace chunk received
|
||||
- `Tracing.tracingComplete` - Recording finished
|
||||
|
||||
**Use Cases:**
|
||||
- Deep performance analysis
|
||||
- Frame rendering profiling
|
||||
- CPU flame graphs
|
||||
- Memory profiling
|
||||
|
||||
---
|
||||
|
||||
### Profiler
|
||||
**Purpose:** CPU profiling
|
||||
|
||||
**Key Commands:**
|
||||
- `Profiler.enable()` - Enable profiler
|
||||
- `Profiler.start()` - Start CPU profiling
|
||||
- `Profiler.stop()` - Stop and get profile
|
||||
|
||||
**Use Cases:**
|
||||
- Find CPU bottlenecks
|
||||
- Optimize JavaScript
|
||||
- Generate flame graphs
|
||||
|
||||
---
|
||||
|
||||
### HeapProfiler (via Memory domain)
|
||||
**Purpose:** Memory profiling
|
||||
|
||||
**Key Commands:**
|
||||
- `Memory.getDOMCounters()` - Get DOM object counts
|
||||
- `Memory.prepareForLeakDetection()` - Prepare leak detection
|
||||
- `Memory.forciblyPurgeJavaScriptMemory()` - Force GC
|
||||
- `Memory.setPressureNotificationsSuppressed(suppressed)` - Control memory warnings
|
||||
- `Memory.simulatePressureNotification(level)` - Simulate memory pressure
|
||||
|
||||
**Use Cases:**
|
||||
- Detect memory leaks
|
||||
- Analyze heap snapshots
|
||||
- Monitor object counts
|
||||
|
||||
---
|
||||
|
||||
## Emulation & Simulation Domains
|
||||
|
||||
### Emulation
|
||||
**Purpose:** Emulate device conditions
|
||||
|
||||
**Key Commands:**
|
||||
- `Emulation.setDeviceMetricsOverride(width, height, deviceScaleFactor, mobile)` - Emulate device
|
||||
- `Emulation.setGeolocationOverride(latitude, longitude, accuracy)` - Fake location
|
||||
- `Emulation.setEmulatedMedia(media, features)` - Emulate media type
|
||||
- `Emulation.setTimezoneOverride(timezoneId)` - Override timezone
|
||||
- `Emulation.setLocaleOverride(locale)` - Override language
|
||||
- `Emulation.setUserAgentOverride(userAgent)` - Change user agent
|
||||
|
||||
**Use Cases:**
|
||||
- Mobile device testing
|
||||
- Geolocation testing
|
||||
- Print media emulation
|
||||
- Timezone/locale testing
|
||||
|
||||
---
|
||||
|
||||
### DeviceOrientation
|
||||
**Purpose:** Simulate device orientation
|
||||
|
||||
**Key Commands:**
|
||||
- `DeviceOrientation.setDeviceOrientationOverride(alpha, beta, gamma)` - Set orientation
|
||||
|
||||
**Use Cases:**
|
||||
- Test accelerometer features
|
||||
- Orientation-dependent layouts
|
||||
|
||||
---
|
||||
|
||||
## Worker & Service Domains
|
||||
|
||||
### ServiceWorker
|
||||
**Purpose:** Manage service workers
|
||||
|
||||
**Key Commands:**
|
||||
- `ServiceWorker.enable()` - Enable tracking
|
||||
- `ServiceWorker.unregister(scopeURL)` - Unregister worker
|
||||
- `ServiceWorker.startWorker(scopeURL)` - Start worker
|
||||
- `ServiceWorker.stopWorker(versionId)` - Stop worker
|
||||
- `ServiceWorker.inspectWorker(versionId)` - Debug worker
|
||||
|
||||
**Key Events:**
|
||||
- `ServiceWorker.workerRegistrationUpdated` - Registration changed
|
||||
- `ServiceWorker.workerVersionUpdated` - Version updated
|
||||
|
||||
---
|
||||
|
||||
### WebAuthn
|
||||
**Purpose:** Simulate WebAuthn/FIDO2
|
||||
|
||||
**Key Commands:**
|
||||
- `WebAuthn.enable()` - Enable virtual authenticators
|
||||
- `WebAuthn.addVirtualAuthenticator(options)` - Add virtual device
|
||||
- `WebAuthn.removeVirtualAuthenticator(authenticatorId)` - Remove device
|
||||
- `WebAuthn.addCredential(authenticatorId, credential)` - Add credential
|
||||
|
||||
**Use Cases:**
|
||||
- Test WebAuthn flows
|
||||
- Simulate biometric auth
|
||||
- Test security keys
|
||||
|
||||
---
|
||||
|
||||
## Developer Tools Support
|
||||
|
||||
### Inspector
|
||||
**Purpose:** Protocol-level debugging
|
||||
|
||||
**Key Events:**
|
||||
- `Inspector.detached` - Debugger disconnected
|
||||
- `Inspector.targetCrashed` - Target crashed
|
||||
|
||||
---
|
||||
|
||||
### Log
|
||||
**Purpose:** Collect browser logs
|
||||
|
||||
**Key Commands:**
|
||||
- `Log.enable()` - Enable log collection
|
||||
- `Log.clear()` - Clear logs
|
||||
|
||||
**Key Events:**
|
||||
- `Log.entryAdded` - New log entry
|
||||
|
||||
**Use Cases:**
|
||||
- Collect console logs
|
||||
- Monitor violations
|
||||
- Track deprecations
|
||||
|
||||
---
|
||||
|
||||
### DOMDebugger
|
||||
**Purpose:** DOM-level debugging
|
||||
|
||||
**Key Commands:**
|
||||
- `DOMDebugger.setDOMBreakpoint(nodeId, type)` - Break on DOM changes
|
||||
- `DOMDebugger.setEventListenerBreakpoint(eventName)` - Break on event
|
||||
- `DOMDebugger.setXHRBreakpoint(url)` - Break on XHR
|
||||
|
||||
**Breakpoint Types:**
|
||||
- subtree-modified, attribute-modified, node-removed
|
||||
|
||||
---
|
||||
|
||||
### DOMSnapshot
|
||||
**Purpose:** Capture complete DOM snapshot
|
||||
|
||||
**Key Commands:**
|
||||
- `DOMSnapshot.captureSnapshot(computedStyles)` - Capture full DOM
|
||||
|
||||
**Use Cases:**
|
||||
- Export page structure
|
||||
- Offline analysis
|
||||
- DOM diffing
|
||||
|
||||
---
|
||||
|
||||
### Audits (Lighthouse Integration)
|
||||
**Purpose:** Run automated audits
|
||||
|
||||
**Key Commands:**
|
||||
- `Audits.enable()` - Enable audits
|
||||
- `Audits.getEncodingIssues()` - Check encoding issues
|
||||
|
||||
---
|
||||
|
||||
### LayerTree
|
||||
**Purpose:** Inspect rendering layers
|
||||
|
||||
**Key Commands:**
|
||||
- `LayerTree.enable()` - Enable layer tracking
|
||||
- `LayerTree.compositingReasons(layerId)` - Get why layer created
|
||||
|
||||
**Key Events:**
|
||||
- `LayerTree.layerTreeDidChange` - Layers changed
|
||||
|
||||
**Use Cases:**
|
||||
- Debug rendering performance
|
||||
- Identify layer creation
|
||||
- Optimize compositing
|
||||
|
||||
---
|
||||
|
||||
## Other Domains
|
||||
|
||||
### Browser
|
||||
**Purpose:** Browser-level control
|
||||
|
||||
**Key Commands:**
|
||||
- `Browser.getVersion()` - Get browser info
|
||||
- `Browser.getBrowserCommandLine()` - Get launch args
|
||||
- `Browser.setPermission(permission, setting, origin)` - Set permissions
|
||||
- `Browser.grantPermissions(permissions, origin)` - Grant permissions
|
||||
|
||||
**Permissions:**
|
||||
- geolocation, midi, notifications, push, camera, microphone, background-sync, sensors, accessibility-events, clipboard-read, clipboard-write, payment-handler
|
||||
|
||||
---
|
||||
|
||||
### IO
|
||||
**Purpose:** File I/O operations
|
||||
|
||||
**Key Commands:**
|
||||
- `IO.read(handle, offset, size)` - Read stream
|
||||
- `IO.close(handle)` - Close stream
|
||||
|
||||
**Use Cases:**
|
||||
- Read large response bodies
|
||||
- Process binary data
|
||||
|
||||
---
|
||||
|
||||
### Media
|
||||
**Purpose:** Inspect media players
|
||||
|
||||
**Key Commands:**
|
||||
- `Media.enable()` - Track media players
|
||||
|
||||
**Key Events:**
|
||||
- `Media.playerPropertiesChanged` - Player state changed
|
||||
- `Media.playerEventsAdded` - Player events
|
||||
|
||||
---
|
||||
|
||||
### BackgroundService
|
||||
**Purpose:** Track background services
|
||||
|
||||
**Key Commands:**
|
||||
- `BackgroundService.startObserving(service)` - Track service
|
||||
|
||||
**Services:**
|
||||
- backgroundFetch, backgroundSync, pushMessaging, notifications, paymentHandler, periodicBackgroundSync
|
||||
|
||||
---
|
||||
|
||||
## Domain Dependencies
|
||||
|
||||
Some domains depend on others and must be enabled in order:
|
||||
|
||||
```
|
||||
Runtime (no dependencies)
|
||||
↓
|
||||
DOM (depends on Runtime)
|
||||
↓
|
||||
CSS (depends on DOM)
|
||||
|
||||
Network (no dependencies)
|
||||
|
||||
Page (depends on Runtime)
|
||||
↓
|
||||
Target (depends on Page)
|
||||
|
||||
Debugger (depends on Runtime)
|
||||
```
|
||||
|
||||
## Quick Command Reference
|
||||
|
||||
### Most Common Commands
|
||||
|
||||
```javascript
|
||||
// Navigation
|
||||
Page.navigate(url)
|
||||
Page.reload()
|
||||
|
||||
// JavaScript Execution
|
||||
Runtime.evaluate(expression)
|
||||
|
||||
// DOM Access
|
||||
DOM.getDocument()
|
||||
DOM.querySelector(nodeId, selector)
|
||||
|
||||
// Screenshots
|
||||
Page.captureScreenshot(format, quality)
|
||||
|
||||
// Network Monitoring
|
||||
Network.enable()
|
||||
// Listen for Network.requestWillBeSent events
|
||||
|
||||
// Console Messages
|
||||
// Listen for Runtime.consoleAPICalled events
|
||||
|
||||
// Cookies
|
||||
Network.getCookies(urls)
|
||||
Network.setCookie(...)
|
||||
|
||||
// Device Emulation
|
||||
Emulation.setDeviceMetricsOverride(width, height, ...)
|
||||
|
||||
// Performance
|
||||
Performance.getMetrics()
|
||||
Tracing.start(categories)
|
||||
Tracing.end()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Enable domains before use:** Always call `.enable()` for stateful domains
|
||||
2. **Handle events:** Subscribe to events for real-time updates
|
||||
3. **Clean up:** Disable domains when done to reduce overhead
|
||||
4. **Use sessions:** Attach to specific targets for isolated debugging
|
||||
5. **Handle errors:** Implement proper error handling for command failures
|
||||
6. **Version awareness:** Check browser version for experimental API support
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/) - Interactive domain browser
|
||||
- [Protocol JSON](https://chromedevtools.github.io/devtools-protocol/tot/json) - Machine-readable specification
|
||||
- [Getting Started with CDP](https://github.com/aslushnikov/getting-started-with-cdp)
|
||||
- [devtools-protocol NPM](https://www.npmjs.com/package/devtools-protocol) - TypeScript definitions
|
||||
940
skills/chrome-devtools/references/performance-guide.md
Normal file
940
skills/chrome-devtools/references/performance-guide.md
Normal file
@@ -0,0 +1,940 @@
|
||||
# Performance Analysis Guide
|
||||
|
||||
Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools skill.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Core Web Vitals](#core-web-vitals)
|
||||
- [Performance Tracing](#performance-tracing)
|
||||
- [Network Analysis](#network-analysis)
|
||||
- [JavaScript Performance](#javascript-performance)
|
||||
- [Rendering Performance](#rendering-performance)
|
||||
- [Memory Analysis](#memory-analysis)
|
||||
- [Optimization Strategies](#optimization-strategies)
|
||||
|
||||
---
|
||||
|
||||
## Core Web Vitals
|
||||
|
||||
### Overview
|
||||
|
||||
Core Web Vitals are Google's standardized metrics for measuring user experience:
|
||||
|
||||
- **LCP (Largest Contentful Paint)** - Loading performance (< 2.5s good)
|
||||
- **FID (First Input Delay)** - Interactivity (< 100ms good)
|
||||
- **CLS (Cumulative Layout Shift)** - Visual stability (< 0.1 good)
|
||||
|
||||
### Measuring with chrome-devtools-mcp
|
||||
|
||||
```javascript
|
||||
// Start performance trace
|
||||
await useTool('performance_start_trace', {
|
||||
categories: ['loading', 'rendering', 'scripting']
|
||||
});
|
||||
|
||||
// Navigate to page
|
||||
await useTool('navigate_page', {
|
||||
url: 'https://example.com'
|
||||
});
|
||||
|
||||
// Wait for complete load
|
||||
await useTool('wait_for', {
|
||||
waitUntil: 'networkidle'
|
||||
});
|
||||
|
||||
// Stop trace and get data
|
||||
await useTool('performance_stop_trace');
|
||||
|
||||
// Get AI-powered insights
|
||||
const insights = await useTool('performance_analyze_insight');
|
||||
|
||||
// insights will include:
|
||||
// - LCP timing
|
||||
// - FID analysis
|
||||
// - CLS score
|
||||
// - Performance recommendations
|
||||
```
|
||||
|
||||
### Measuring with Puppeteer
|
||||
|
||||
```javascript
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Measure Core Web Vitals
|
||||
await page.goto('https://example.com', {
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
const vitals = await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const vitals = {
|
||||
LCP: null,
|
||||
FID: null,
|
||||
CLS: 0
|
||||
};
|
||||
|
||||
// LCP
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
vitals.LCP = entries[entries.length - 1].renderTime ||
|
||||
entries[entries.length - 1].loadTime;
|
||||
}).observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
|
||||
// FID
|
||||
new PerformanceObserver((list) => {
|
||||
vitals.FID = list.getEntries()[0].processingStart -
|
||||
list.getEntries()[0].startTime;
|
||||
}).observe({ entryTypes: ['first-input'] });
|
||||
|
||||
// CLS
|
||||
new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
vitals.CLS += entry.value;
|
||||
}
|
||||
});
|
||||
}).observe({ entryTypes: ['layout-shift'] });
|
||||
|
||||
// Wait 5 seconds for metrics
|
||||
setTimeout(() => resolve(vitals), 5000);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Core Web Vitals:', vitals);
|
||||
```
|
||||
|
||||
### Other Important Metrics
|
||||
|
||||
**TTFB (Time to First Byte)**
|
||||
```javascript
|
||||
const ttfb = await page.evaluate(() => {
|
||||
const [navigationEntry] = performance.getEntriesByType('navigation');
|
||||
return navigationEntry.responseStart - navigationEntry.requestStart;
|
||||
});
|
||||
```
|
||||
|
||||
**FCP (First Contentful Paint)**
|
||||
```javascript
|
||||
const fcp = await page.evaluate(() => {
|
||||
const paintEntries = performance.getEntriesByType('paint');
|
||||
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
|
||||
return fcpEntry ? fcpEntry.startTime : null;
|
||||
});
|
||||
```
|
||||
|
||||
**TTI (Time to Interactive)**
|
||||
```javascript
|
||||
// Requires lighthouse or manual calculation
|
||||
const tti = await page.evaluate(() => {
|
||||
// Complex calculation based on network idle and long tasks
|
||||
// Best to use Lighthouse for accurate TTI
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Tracing
|
||||
|
||||
### Chrome Trace Categories
|
||||
|
||||
**Loading:**
|
||||
- Page load events
|
||||
- Resource loading
|
||||
- Parser activity
|
||||
|
||||
**Rendering:**
|
||||
- Layout calculations
|
||||
- Paint operations
|
||||
- Compositing
|
||||
|
||||
**Scripting:**
|
||||
- JavaScript execution
|
||||
- V8 compilation
|
||||
- Garbage collection
|
||||
|
||||
**Network:**
|
||||
- HTTP requests
|
||||
- WebSocket traffic
|
||||
- Resource fetching
|
||||
|
||||
**Input:**
|
||||
- User input processing
|
||||
- Touch/scroll events
|
||||
|
||||
**GPU:**
|
||||
- GPU operations
|
||||
- Compositing work
|
||||
|
||||
### Record Performance Trace
|
||||
|
||||
**Using chrome-devtools-mcp:**
|
||||
```javascript
|
||||
// Start trace with specific categories
|
||||
await useTool('performance_start_trace', {
|
||||
categories: ['loading', 'rendering', 'scripting', 'network']
|
||||
});
|
||||
|
||||
// Perform actions
|
||||
await useTool('navigate_page', { url: 'https://example.com' });
|
||||
await useTool('wait_for', { waitUntil: 'networkidle' });
|
||||
|
||||
// Optional: Interact with page
|
||||
await useTool('click', { uid: 'button-uid' });
|
||||
|
||||
// Stop trace
|
||||
const traceData = await useTool('performance_stop_trace');
|
||||
|
||||
// Analyze trace
|
||||
const insights = await useTool('performance_analyze_insight');
|
||||
```
|
||||
|
||||
**Using Puppeteer:**
|
||||
```javascript
|
||||
// Start tracing
|
||||
await page.tracing.start({
|
||||
path: 'trace.json',
|
||||
categories: [
|
||||
'devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline',
|
||||
'disabled-by-default-v8.cpu_profiler'
|
||||
]
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto('https://example.com', {
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
// Stop tracing
|
||||
await page.tracing.stop();
|
||||
|
||||
// Analyze in Chrome DevTools (chrome://tracing)
|
||||
```
|
||||
|
||||
### Analyze Trace Data
|
||||
|
||||
**Key Metrics from Trace:**
|
||||
|
||||
1. **Main Thread Activity**
|
||||
- JavaScript execution time
|
||||
- Layout/reflow time
|
||||
- Paint time
|
||||
- Long tasks (> 50ms)
|
||||
|
||||
2. **Network Waterfall**
|
||||
- Request start times
|
||||
- DNS lookup
|
||||
- Connection time
|
||||
- Download time
|
||||
|
||||
3. **Rendering Pipeline**
|
||||
- DOM construction
|
||||
- Style calculation
|
||||
- Layout
|
||||
- Paint
|
||||
- Composite
|
||||
|
||||
**Common Issues to Look For:**
|
||||
- Long tasks blocking main thread
|
||||
- Excessive JavaScript execution
|
||||
- Layout thrashing
|
||||
- Unnecessary repaints
|
||||
- Slow network requests
|
||||
- Large bundle sizes
|
||||
|
||||
---
|
||||
|
||||
## Network Analysis
|
||||
|
||||
### Monitor Network Requests
|
||||
|
||||
**Using chrome-devtools-mcp:**
|
||||
```javascript
|
||||
// Navigate to page
|
||||
await useTool('navigate_page', { url: 'https://example.com' });
|
||||
|
||||
// Wait for all requests
|
||||
await useTool('wait_for', { waitUntil: 'networkidle' });
|
||||
|
||||
// List all requests
|
||||
const requests = await useTool('list_network_requests', {
|
||||
resourceTypes: ['Document', 'Script', 'Stylesheet', 'Image', 'XHR', 'Fetch'],
|
||||
pageSize: 100
|
||||
});
|
||||
|
||||
// Analyze specific request
|
||||
for (const req of requests.requests) {
|
||||
const details = await useTool('get_network_request', {
|
||||
requestId: req.id
|
||||
});
|
||||
|
||||
console.log({
|
||||
url: details.url,
|
||||
method: details.method,
|
||||
status: details.status,
|
||||
size: details.encodedDataLength,
|
||||
time: details.timing.receiveHeadersEnd - details.timing.requestTime,
|
||||
cached: details.fromCache
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Using Puppeteer:**
|
||||
```javascript
|
||||
const requests = [];
|
||||
|
||||
// Capture all requests
|
||||
page.on('request', (request) => {
|
||||
requests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
resourceType: request.resourceType(),
|
||||
headers: request.headers()
|
||||
});
|
||||
});
|
||||
|
||||
// Capture responses
|
||||
page.on('response', (response) => {
|
||||
const request = response.request();
|
||||
console.log({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
size: response.headers()['content-length'],
|
||||
cached: response.fromCache(),
|
||||
timing: response.timing()
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('https://example.com');
|
||||
```
|
||||
|
||||
### Network Performance Metrics
|
||||
|
||||
**Calculate Total Page Weight:**
|
||||
```javascript
|
||||
let totalBytes = 0;
|
||||
let resourceCounts = {};
|
||||
|
||||
page.on('response', async (response) => {
|
||||
const type = response.request().resourceType();
|
||||
const buffer = await response.buffer();
|
||||
|
||||
totalBytes += buffer.length;
|
||||
resourceCounts[type] = (resourceCounts[type] || 0) + 1;
|
||||
});
|
||||
|
||||
await page.goto('https://example.com');
|
||||
|
||||
console.log('Total size:', (totalBytes / 1024 / 1024).toFixed(2), 'MB');
|
||||
console.log('Resources:', resourceCounts);
|
||||
```
|
||||
|
||||
**Identify Slow Requests:**
|
||||
```javascript
|
||||
page.on('response', (response) => {
|
||||
const timing = response.timing();
|
||||
const totalTime = timing.receiveHeadersEnd - timing.requestTime;
|
||||
|
||||
if (totalTime > 1000) { // Slower than 1 second
|
||||
console.log('Slow request:', {
|
||||
url: response.url(),
|
||||
time: totalTime.toFixed(2) + 'ms',
|
||||
size: response.headers()['content-length']
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Network Throttling
|
||||
|
||||
**Simulate Slow Connection:**
|
||||
```javascript
|
||||
// Using chrome-devtools-mcp
|
||||
await useTool('emulate_network', {
|
||||
throttlingOption: 'Slow 3G' // or 'Fast 3G', 'Slow 4G'
|
||||
});
|
||||
|
||||
// Using Puppeteer
|
||||
const client = await page.createCDPSession();
|
||||
await client.send('Network.emulateNetworkConditions', {
|
||||
offline: false,
|
||||
downloadThroughput: 400 * 1024 / 8, // 400 Kbps
|
||||
uploadThroughput: 400 * 1024 / 8,
|
||||
latency: 2000 // 2000ms RTT
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript Performance
|
||||
|
||||
### Identify Long Tasks
|
||||
|
||||
**Using Performance Observer:**
|
||||
```javascript
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const longTasks = [];
|
||||
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
longTasks.push({
|
||||
name: entry.name,
|
||||
duration: entry.duration,
|
||||
startTime: entry.startTime
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe({ entryTypes: ['longtask'] });
|
||||
|
||||
// Collect for 10 seconds
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve(longTasks);
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### CPU Profiling
|
||||
|
||||
**Using Puppeteer:**
|
||||
```javascript
|
||||
// Start CPU profiling
|
||||
const client = await page.createCDPSession();
|
||||
await client.send('Profiler.enable');
|
||||
await client.send('Profiler.start');
|
||||
|
||||
// Navigate and interact
|
||||
await page.goto('https://example.com');
|
||||
await page.click('.button');
|
||||
|
||||
// Stop profiling
|
||||
const { profile } = await client.send('Profiler.stop');
|
||||
|
||||
// Analyze profile (flame graph data)
|
||||
// Import into Chrome DevTools for visualization
|
||||
```
|
||||
|
||||
### JavaScript Coverage
|
||||
|
||||
**Identify Unused Code:**
|
||||
```javascript
|
||||
// Start coverage
|
||||
await Promise.all([
|
||||
page.coverage.startJSCoverage(),
|
||||
page.coverage.startCSSCoverage()
|
||||
]);
|
||||
|
||||
// Navigate
|
||||
await page.goto('https://example.com');
|
||||
|
||||
// Stop coverage
|
||||
const [jsCoverage, cssCoverage] = await Promise.all([
|
||||
page.coverage.stopJSCoverage(),
|
||||
page.coverage.stopCSSCoverage()
|
||||
]);
|
||||
|
||||
// Calculate unused bytes
|
||||
function calculateUnusedBytes(coverage) {
|
||||
let usedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
for (const entry of coverage) {
|
||||
totalBytes += entry.text.length;
|
||||
for (const range of entry.ranges) {
|
||||
usedBytes += range.end - range.start - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
usedBytes,
|
||||
totalBytes,
|
||||
unusedBytes: totalBytes - usedBytes,
|
||||
unusedPercentage: ((totalBytes - usedBytes) / totalBytes * 100).toFixed(2)
|
||||
};
|
||||
}
|
||||
|
||||
console.log('JS Coverage:', calculateUnusedBytes(jsCoverage));
|
||||
console.log('CSS Coverage:', calculateUnusedBytes(cssCoverage));
|
||||
```
|
||||
|
||||
### Bundle Size Analysis
|
||||
|
||||
**Analyze JavaScript Bundles:**
|
||||
```javascript
|
||||
page.on('response', async (response) => {
|
||||
const url = response.url();
|
||||
const type = response.request().resourceType();
|
||||
|
||||
if (type === 'script') {
|
||||
const buffer = await response.buffer();
|
||||
const size = buffer.length;
|
||||
|
||||
console.log({
|
||||
url: url.split('/').pop(),
|
||||
size: (size / 1024).toFixed(2) + ' KB',
|
||||
gzipped: response.headers()['content-encoding'] === 'gzip'
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rendering Performance
|
||||
|
||||
### Layout Thrashing Detection
|
||||
|
||||
**Monitor Layout Recalculations:**
|
||||
```javascript
|
||||
// Using Performance Observer
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const measurements = [];
|
||||
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
if (entry.entryType === 'measure' &&
|
||||
entry.name.includes('layout')) {
|
||||
measurements.push({
|
||||
name: entry.name,
|
||||
duration: entry.duration,
|
||||
startTime: entry.startTime
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe({ entryTypes: ['measure'] });
|
||||
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve(measurements);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Paint and Composite Metrics
|
||||
|
||||
**Get Paint Metrics:**
|
||||
```javascript
|
||||
const paintMetrics = await page.evaluate(() => {
|
||||
const paints = performance.getEntriesByType('paint');
|
||||
return {
|
||||
firstPaint: paints.find(p => p.name === 'first-paint')?.startTime,
|
||||
firstContentfulPaint: paints.find(p => p.name === 'first-contentful-paint')?.startTime
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Frame Rate Analysis
|
||||
|
||||
**Monitor FPS:**
|
||||
```javascript
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
let frames = 0;
|
||||
let lastTime = performance.now();
|
||||
|
||||
function countFrames() {
|
||||
frames++;
|
||||
requestAnimationFrame(countFrames);
|
||||
}
|
||||
|
||||
countFrames();
|
||||
|
||||
setTimeout(() => {
|
||||
const now = performance.now();
|
||||
const elapsed = (now - lastTime) / 1000;
|
||||
const fps = frames / elapsed;
|
||||
resolve(fps);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Layout Shifts (CLS)
|
||||
|
||||
**Track Individual Shifts:**
|
||||
```javascript
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const shifts = [];
|
||||
let totalCLS = 0;
|
||||
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
totalCLS += entry.value;
|
||||
shifts.push({
|
||||
value: entry.value,
|
||||
time: entry.startTime,
|
||||
elements: entry.sources?.map(s => s.node)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe({ entryTypes: ['layout-shift'] });
|
||||
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve({ totalCLS, shifts });
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Analysis
|
||||
|
||||
### Memory Metrics
|
||||
|
||||
**Get Memory Usage:**
|
||||
```javascript
|
||||
// Using chrome-devtools-mcp
|
||||
await useTool('evaluate_script', {
|
||||
expression: `
|
||||
({
|
||||
usedJSHeapSize: performance.memory?.usedJSHeapSize,
|
||||
totalJSHeapSize: performance.memory?.totalJSHeapSize,
|
||||
jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit
|
||||
})
|
||||
`,
|
||||
returnByValue: true
|
||||
});
|
||||
|
||||
// Using Puppeteer
|
||||
const metrics = await page.metrics();
|
||||
console.log({
|
||||
jsHeapUsed: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2) + ' MB',
|
||||
jsHeapTotal: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) + ' MB',
|
||||
domNodes: metrics.Nodes,
|
||||
documents: metrics.Documents,
|
||||
jsEventListeners: metrics.JSEventListeners
|
||||
});
|
||||
```
|
||||
|
||||
### Memory Leak Detection
|
||||
|
||||
**Monitor Memory Over Time:**
|
||||
```javascript
|
||||
async function detectMemoryLeak(page, duration = 30000) {
|
||||
const samples = [];
|
||||
const interval = 1000; // Sample every second
|
||||
const samples_count = duration / interval;
|
||||
|
||||
for (let i = 0; i < samples_count; i++) {
|
||||
const metrics = await page.metrics();
|
||||
samples.push({
|
||||
time: i,
|
||||
heapUsed: metrics.JSHeapUsedSize
|
||||
});
|
||||
|
||||
await page.waitForTimeout(interval);
|
||||
}
|
||||
|
||||
// Analyze trend
|
||||
const firstSample = samples[0].heapUsed;
|
||||
const lastSample = samples[samples.length - 1].heapUsed;
|
||||
const increase = ((lastSample - firstSample) / firstSample * 100).toFixed(2);
|
||||
|
||||
return {
|
||||
samples,
|
||||
memoryIncrease: increase + '%',
|
||||
possibleLeak: increase > 50 // > 50% increase indicates possible leak
|
||||
};
|
||||
}
|
||||
|
||||
const leakAnalysis = await detectMemoryLeak(page, 30000);
|
||||
console.log('Memory Analysis:', leakAnalysis);
|
||||
```
|
||||
|
||||
### Heap Snapshot
|
||||
|
||||
**Capture Heap Snapshot:**
|
||||
```javascript
|
||||
const client = await page.createCDPSession();
|
||||
|
||||
// Take snapshot
|
||||
await client.send('HeapProfiler.enable');
|
||||
const { result } = await client.send('HeapProfiler.takeHeapSnapshot');
|
||||
|
||||
// Snapshot is streamed in chunks
|
||||
// Save to file or analyze programmatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optimization Strategies
|
||||
|
||||
### Image Optimization
|
||||
|
||||
**Detect Unoptimized Images:**
|
||||
```javascript
|
||||
const images = await page.evaluate(() => {
|
||||
const images = Array.from(document.querySelectorAll('img'));
|
||||
return images.map(img => ({
|
||||
src: img.src,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
displayWidth: img.width,
|
||||
displayHeight: img.height,
|
||||
oversized: img.naturalWidth > img.width * 1.5 ||
|
||||
img.naturalHeight > img.height * 1.5
|
||||
}));
|
||||
});
|
||||
|
||||
const oversizedImages = images.filter(img => img.oversized);
|
||||
console.log('Oversized images:', oversizedImages);
|
||||
```
|
||||
|
||||
### Font Loading
|
||||
|
||||
**Detect Render-Blocking Fonts:**
|
||||
```javascript
|
||||
const fonts = await page.evaluate(() => {
|
||||
return Array.from(document.fonts).map(font => ({
|
||||
family: font.family,
|
||||
weight: font.weight,
|
||||
style: font.style,
|
||||
status: font.status,
|
||||
loaded: font.status === 'loaded'
|
||||
}));
|
||||
});
|
||||
|
||||
console.log('Fonts:', fonts);
|
||||
```
|
||||
|
||||
### Third-Party Scripts
|
||||
|
||||
**Measure Third-Party Impact:**
|
||||
```javascript
|
||||
const thirdPartyDomains = ['googletagmanager.com', 'facebook.net', 'doubleclick.net'];
|
||||
|
||||
page.on('response', async (response) => {
|
||||
const url = response.url();
|
||||
const isThirdParty = thirdPartyDomains.some(domain => url.includes(domain));
|
||||
|
||||
if (isThirdParty) {
|
||||
const buffer = await response.buffer();
|
||||
console.log({
|
||||
url: url,
|
||||
size: (buffer.length / 1024).toFixed(2) + ' KB',
|
||||
type: response.request().resourceType()
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Critical Rendering Path
|
||||
|
||||
**Identify Render-Blocking Resources:**
|
||||
```javascript
|
||||
await page.goto('https://example.com');
|
||||
|
||||
const renderBlockingResources = await page.evaluate(() => {
|
||||
const resources = performance.getEntriesByType('resource');
|
||||
return resources.filter(resource => {
|
||||
return (resource.initiatorType === 'link' &&
|
||||
resource.name.includes('.css')) ||
|
||||
(resource.initiatorType === 'script' &&
|
||||
!resource.name.includes('async'));
|
||||
}).map(r => ({
|
||||
url: r.name,
|
||||
duration: r.duration,
|
||||
startTime: r.startTime
|
||||
}));
|
||||
});
|
||||
|
||||
console.log('Render-blocking resources:', renderBlockingResources);
|
||||
```
|
||||
|
||||
### Lighthouse Integration
|
||||
|
||||
**Run Lighthouse Audit:**
|
||||
```javascript
|
||||
import lighthouse from 'lighthouse';
|
||||
import { launch } from 'chrome-launcher';
|
||||
|
||||
// Launch Chrome
|
||||
const chrome = await launch({ chromeFlags: ['--headless'] });
|
||||
|
||||
// Run Lighthouse
|
||||
const { lhr } = await lighthouse('https://example.com', {
|
||||
port: chrome.port,
|
||||
onlyCategories: ['performance']
|
||||
});
|
||||
|
||||
// Get scores
|
||||
console.log({
|
||||
performanceScore: lhr.categories.performance.score * 100,
|
||||
metrics: {
|
||||
FCP: lhr.audits['first-contentful-paint'].displayValue,
|
||||
LCP: lhr.audits['largest-contentful-paint'].displayValue,
|
||||
TBT: lhr.audits['total-blocking-time'].displayValue,
|
||||
CLS: lhr.audits['cumulative-layout-shift'].displayValue,
|
||||
SI: lhr.audits['speed-index'].displayValue
|
||||
},
|
||||
opportunities: lhr.audits['opportunities']
|
||||
});
|
||||
|
||||
await chrome.kill();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Budgets
|
||||
|
||||
### Set Performance Budgets
|
||||
|
||||
```javascript
|
||||
const budgets = {
|
||||
// Core Web Vitals
|
||||
LCP: 2500, // ms
|
||||
FID: 100, // ms
|
||||
CLS: 0.1, // score
|
||||
|
||||
// Other metrics
|
||||
FCP: 1800, // ms
|
||||
TTI: 3800, // ms
|
||||
TBT: 300, // ms
|
||||
|
||||
// Resource budgets
|
||||
totalPageSize: 2 * 1024 * 1024, // 2 MB
|
||||
jsSize: 500 * 1024, // 500 KB
|
||||
cssSize: 100 * 1024, // 100 KB
|
||||
imageSize: 1 * 1024 * 1024, // 1 MB
|
||||
|
||||
// Request counts
|
||||
totalRequests: 50,
|
||||
jsRequests: 10,
|
||||
cssRequests: 5
|
||||
};
|
||||
|
||||
async function checkBudgets(page, budgets) {
|
||||
// Measure actual values
|
||||
const vitals = await measureCoreWebVitals(page);
|
||||
const resources = await analyzeResources(page);
|
||||
|
||||
// Compare against budgets
|
||||
const violations = [];
|
||||
|
||||
if (vitals.LCP > budgets.LCP) {
|
||||
violations.push(`LCP: ${vitals.LCP}ms exceeds budget of ${budgets.LCP}ms`);
|
||||
}
|
||||
|
||||
if (resources.totalSize > budgets.totalPageSize) {
|
||||
violations.push(`Page size: ${resources.totalSize} exceeds budget of ${budgets.totalPageSize}`);
|
||||
}
|
||||
|
||||
// ... check other budgets
|
||||
|
||||
return {
|
||||
passed: violations.length === 0,
|
||||
violations
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automated Performance Testing
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```javascript
|
||||
// performance-test.js
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function performanceTest(url) {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Measure metrics
|
||||
await page.goto(url, { waitUntil: 'networkidle2' });
|
||||
const metrics = await page.metrics();
|
||||
const vitals = await measureCoreWebVitals(page);
|
||||
|
||||
await browser.close();
|
||||
|
||||
// Check against thresholds
|
||||
const thresholds = {
|
||||
LCP: 2500,
|
||||
FID: 100,
|
||||
CLS: 0.1,
|
||||
jsHeapSize: 50 * 1024 * 1024 // 50 MB
|
||||
};
|
||||
|
||||
const failed = [];
|
||||
if (vitals.LCP > thresholds.LCP) failed.push('LCP');
|
||||
if (vitals.FID > thresholds.FID) failed.push('FID');
|
||||
if (vitals.CLS > thresholds.CLS) failed.push('CLS');
|
||||
if (metrics.JSHeapUsedSize > thresholds.jsHeapSize) failed.push('Memory');
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.error('Performance test failed:', failed);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Performance test passed');
|
||||
}
|
||||
|
||||
performanceTest(process.env.TEST_URL);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance Testing Checklist
|
||||
|
||||
1. **Measure Multiple Times**
|
||||
- Run tests 3-5 times
|
||||
- Use median values
|
||||
- Account for variance
|
||||
|
||||
2. **Test Different Conditions**
|
||||
- Fast 3G
|
||||
- Slow 3G
|
||||
- Offline
|
||||
- CPU throttling
|
||||
|
||||
3. **Test Different Devices**
|
||||
- Mobile (low-end)
|
||||
- Mobile (high-end)
|
||||
- Desktop
|
||||
- Tablet
|
||||
|
||||
4. **Monitor Over Time**
|
||||
- Track metrics in CI/CD
|
||||
- Set up alerts for regressions
|
||||
- Create performance dashboards
|
||||
|
||||
5. **Focus on User Experience**
|
||||
- Prioritize Core Web Vitals
|
||||
- Test real user journeys
|
||||
- Consider perceived performance
|
||||
|
||||
6. **Optimize Critical Path**
|
||||
- Minimize render-blocking resources
|
||||
- Defer non-critical JavaScript
|
||||
- Optimize font loading
|
||||
- Lazy load images
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Web.dev Performance](https://web.dev/performance/)
|
||||
- [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/)
|
||||
- [Core Web Vitals](https://web.dev/vitals/)
|
||||
- [Lighthouse](https://developer.chrome.com/docs/lighthouse/)
|
||||
- [WebPageTest](https://www.webpagetest.org/)
|
||||
953
skills/chrome-devtools/references/puppeteer-reference.md
Normal file
953
skills/chrome-devtools/references/puppeteer-reference.md
Normal file
@@ -0,0 +1,953 @@
|
||||
# Puppeteer Quick Reference
|
||||
|
||||
Complete guide to browser automation with Puppeteer - a high-level API over Chrome DevTools Protocol.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Setup](#setup)
|
||||
- [Browser & Page Management](#browser--page-management)
|
||||
- [Navigation](#navigation)
|
||||
- [Element Interaction](#element-interaction)
|
||||
- [JavaScript Execution](#javascript-execution)
|
||||
- [Screenshots & PDFs](#screenshots--pdfs)
|
||||
- [Network Interception](#network-interception)
|
||||
- [Device Emulation](#device-emulation)
|
||||
- [Performance](#performance)
|
||||
- [Common Patterns](#common-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install Puppeteer
|
||||
npm install puppeteer
|
||||
|
||||
# Install core only (bring your own Chrome)
|
||||
npm install puppeteer-core
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
// Launch browser
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox']
|
||||
});
|
||||
|
||||
// Open page
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Navigate
|
||||
await page.goto('https://example.com');
|
||||
|
||||
// Do work...
|
||||
|
||||
// Cleanup
|
||||
await browser.close();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browser & Page Management
|
||||
|
||||
### Launch Browser
|
||||
|
||||
```javascript
|
||||
const browser = await puppeteer.launch({
|
||||
// Visibility
|
||||
headless: false, // Show browser UI
|
||||
headless: 'new', // New headless mode (Chrome 112+)
|
||||
|
||||
// Chrome location
|
||||
executablePath: '/path/to/chrome',
|
||||
channel: 'chrome', // or 'chrome-canary', 'chrome-beta'
|
||||
|
||||
// Browser context
|
||||
userDataDir: './user-data', // Persistent profile
|
||||
|
||||
// Window size
|
||||
defaultViewport: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: false
|
||||
},
|
||||
|
||||
// Advanced options
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-web-security',
|
||||
'--disable-features=IsolateOrigins',
|
||||
'--disable-site-isolation-trials',
|
||||
'--start-maximized'
|
||||
],
|
||||
|
||||
// Debugging
|
||||
devtools: true, // Open DevTools automatically
|
||||
slowMo: 250, // Slow down by 250ms per action
|
||||
|
||||
// Network
|
||||
proxy: {
|
||||
server: 'http://proxy.com:8080'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Connect to Running Browser
|
||||
|
||||
```javascript
|
||||
// Launch Chrome with debugging
|
||||
// google-chrome --remote-debugging-port=9222
|
||||
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: 'http://localhost:9222',
|
||||
// or browserWSEndpoint: 'ws://localhost:9222/devtools/browser/...'
|
||||
});
|
||||
```
|
||||
|
||||
### Page Management
|
||||
|
||||
```javascript
|
||||
// Create new page
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Get all pages
|
||||
const pages = await browser.pages();
|
||||
|
||||
// Close page
|
||||
await page.close();
|
||||
|
||||
// Multiple pages
|
||||
const page1 = await browser.newPage();
|
||||
const page2 = await browser.newPage();
|
||||
|
||||
// Switch between pages
|
||||
await page1.bringToFront();
|
||||
```
|
||||
|
||||
### Browser Context (Incognito)
|
||||
|
||||
```javascript
|
||||
// Create isolated context
|
||||
const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Cleanup context
|
||||
await context.close();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
### Basic Navigation
|
||||
|
||||
```javascript
|
||||
// Navigate to URL
|
||||
await page.goto('https://example.com');
|
||||
|
||||
// Navigate with options
|
||||
await page.goto('https://example.com', {
|
||||
waitUntil: 'networkidle2', // or 'load', 'domcontentloaded', 'networkidle0'
|
||||
timeout: 30000 // Max wait time (ms)
|
||||
});
|
||||
|
||||
// Reload page
|
||||
await page.reload({ waitUntil: 'networkidle2' });
|
||||
|
||||
// Navigation history
|
||||
await page.goBack();
|
||||
await page.goForward();
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForNavigation({
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
```
|
||||
|
||||
### Wait Until Options
|
||||
|
||||
- `load` - Wait for load event
|
||||
- `domcontentloaded` - Wait for DOMContentLoaded event
|
||||
- `networkidle0` - Wait until no network connections for 500ms
|
||||
- `networkidle2` - Wait until max 2 network connections for 500ms
|
||||
|
||||
---
|
||||
|
||||
## Element Interaction
|
||||
|
||||
### Selectors
|
||||
|
||||
```javascript
|
||||
// CSS selectors
|
||||
await page.$('#id');
|
||||
await page.$('.class');
|
||||
await page.$('div > p');
|
||||
|
||||
// XPath
|
||||
await page.$x('//button[text()="Submit"]');
|
||||
|
||||
// Get all matching elements
|
||||
await page.$$('.item');
|
||||
await page.$$x('//div[@class="item"]');
|
||||
```
|
||||
|
||||
### Click Elements
|
||||
|
||||
```javascript
|
||||
// Click by selector
|
||||
await page.click('.button');
|
||||
|
||||
// Click with options
|
||||
await page.click('.button', {
|
||||
button: 'left', // or 'right', 'middle'
|
||||
clickCount: 1, // 2 for double-click
|
||||
delay: 100 // Delay between mousedown and mouseup
|
||||
});
|
||||
|
||||
// ElementHandle click
|
||||
const button = await page.$('.button');
|
||||
await button.click();
|
||||
```
|
||||
|
||||
### Type Text
|
||||
|
||||
```javascript
|
||||
// Type into input
|
||||
await page.type('#search', 'query text');
|
||||
|
||||
// Type with delay
|
||||
await page.type('#search', 'slow typing', { delay: 100 });
|
||||
|
||||
// Clear and type
|
||||
await page.$eval('#search', el => el.value = '');
|
||||
await page.type('#search', 'new text');
|
||||
```
|
||||
|
||||
### Form Interaction
|
||||
|
||||
```javascript
|
||||
// Fill input
|
||||
await page.type('#username', 'john@example.com');
|
||||
await page.type('#password', 'secret123');
|
||||
|
||||
// Select dropdown option
|
||||
await page.select('#country', 'US'); // By value
|
||||
await page.select('#country', 'USA', 'UK'); // Multiple
|
||||
|
||||
// Check/uncheck checkbox
|
||||
await page.click('input[type="checkbox"]');
|
||||
|
||||
// Choose radio button
|
||||
await page.click('input[value="option2"]');
|
||||
|
||||
// Upload file
|
||||
const input = await page.$('input[type="file"]');
|
||||
await input.uploadFile('/path/to/file.pdf');
|
||||
|
||||
// Submit form
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForNavigation();
|
||||
```
|
||||
|
||||
### Hover & Focus
|
||||
|
||||
```javascript
|
||||
// Hover over element
|
||||
await page.hover('.menu-item');
|
||||
|
||||
// Focus element
|
||||
await page.focus('#input');
|
||||
|
||||
// Blur
|
||||
await page.$eval('#input', el => el.blur());
|
||||
```
|
||||
|
||||
### Drag & Drop
|
||||
|
||||
```javascript
|
||||
const source = await page.$('.draggable');
|
||||
const target = await page.$('.drop-zone');
|
||||
|
||||
await source.drag(target);
|
||||
await source.drop(target);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript Execution
|
||||
|
||||
### Evaluate in Page Context
|
||||
|
||||
```javascript
|
||||
// Execute JavaScript
|
||||
const title = await page.evaluate(() => document.title);
|
||||
|
||||
// With arguments
|
||||
const text = await page.evaluate(
|
||||
(selector) => document.querySelector(selector).textContent,
|
||||
'.heading'
|
||||
);
|
||||
|
||||
// Return complex data
|
||||
const data = await page.evaluate(() => ({
|
||||
title: document.title,
|
||||
url: location.href,
|
||||
cookies: document.cookie
|
||||
}));
|
||||
|
||||
// With ElementHandle
|
||||
const element = await page.$('.button');
|
||||
const text = await page.evaluate(el => el.textContent, element);
|
||||
```
|
||||
|
||||
### Query & Modify DOM
|
||||
|
||||
```javascript
|
||||
// Get element property
|
||||
const value = await page.$eval('#input', el => el.value);
|
||||
|
||||
// Get multiple elements
|
||||
const items = await page.$$eval('.item', elements =>
|
||||
elements.map(el => el.textContent)
|
||||
);
|
||||
|
||||
// Modify element
|
||||
await page.$eval('#input', (el, value) => {
|
||||
el.value = value;
|
||||
}, 'new value');
|
||||
|
||||
// Add class
|
||||
await page.$eval('.element', el => el.classList.add('active'));
|
||||
```
|
||||
|
||||
### Expose Functions
|
||||
|
||||
```javascript
|
||||
// Expose Node.js function to page
|
||||
await page.exposeFunction('md5', (text) =>
|
||||
crypto.createHash('md5').update(text).digest('hex')
|
||||
);
|
||||
|
||||
// Call from page context
|
||||
const hash = await page.evaluate(async () => {
|
||||
return await window.md5('hello world');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screenshots & PDFs
|
||||
|
||||
### Screenshots
|
||||
|
||||
```javascript
|
||||
// Full page screenshot
|
||||
await page.screenshot({
|
||||
path: 'screenshot.png',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
// Viewport screenshot
|
||||
await page.screenshot({
|
||||
path: 'viewport.png',
|
||||
fullPage: false
|
||||
});
|
||||
|
||||
// Element screenshot
|
||||
const element = await page.$('.chart');
|
||||
await element.screenshot({
|
||||
path: 'chart.png'
|
||||
});
|
||||
|
||||
// Screenshot options
|
||||
await page.screenshot({
|
||||
path: 'page.png',
|
||||
type: 'png', // or 'jpeg', 'webp'
|
||||
quality: 80, // JPEG quality (0-100)
|
||||
clip: { // Crop region
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 500,
|
||||
height: 500
|
||||
},
|
||||
omitBackground: true // Transparent background
|
||||
});
|
||||
|
||||
// Screenshot to buffer
|
||||
const buffer = await page.screenshot();
|
||||
```
|
||||
|
||||
### PDF Generation
|
||||
|
||||
```javascript
|
||||
// Generate PDF
|
||||
await page.pdf({
|
||||
path: 'page.pdf',
|
||||
format: 'A4', // or 'Letter', 'Legal', etc.
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '1cm',
|
||||
right: '1cm',
|
||||
bottom: '1cm',
|
||||
left: '1cm'
|
||||
}
|
||||
});
|
||||
|
||||
// Custom page size
|
||||
await page.pdf({
|
||||
path: 'custom.pdf',
|
||||
width: '8.5in',
|
||||
height: '11in',
|
||||
landscape: true
|
||||
});
|
||||
|
||||
// Header and footer
|
||||
await page.pdf({
|
||||
path: 'report.pdf',
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div style="font-size:10px;">Header</div>',
|
||||
footerTemplate: '<div style="font-size:10px;">Page <span class="pageNumber"></span></div>'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Interception
|
||||
|
||||
### Request Interception
|
||||
|
||||
```javascript
|
||||
// Enable request interception
|
||||
await page.setRequestInterception(true);
|
||||
|
||||
// Intercept requests
|
||||
page.on('request', (request) => {
|
||||
// Block specific resource types
|
||||
if (request.resourceType() === 'image') {
|
||||
request.abort();
|
||||
}
|
||||
// Block URLs
|
||||
else if (request.url().includes('ads')) {
|
||||
request.abort();
|
||||
}
|
||||
// Modify request
|
||||
else if (request.url().includes('api')) {
|
||||
request.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'Authorization': 'Bearer token'
|
||||
}
|
||||
});
|
||||
}
|
||||
// Continue normally
|
||||
else {
|
||||
request.continue();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Responses
|
||||
|
||||
```javascript
|
||||
await page.setRequestInterception(true);
|
||||
|
||||
page.on('request', (request) => {
|
||||
if (request.url().includes('/api/user')) {
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
id: 1,
|
||||
name: 'Mock User'
|
||||
})
|
||||
});
|
||||
} else {
|
||||
request.continue();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Monitor Network
|
||||
|
||||
```javascript
|
||||
// Track requests
|
||||
page.on('request', (request) => {
|
||||
console.log('Request:', request.method(), request.url());
|
||||
});
|
||||
|
||||
// Track responses
|
||||
page.on('response', (response) => {
|
||||
console.log('Response:', response.status(), response.url());
|
||||
});
|
||||
|
||||
// Track failed requests
|
||||
page.on('requestfailed', (request) => {
|
||||
console.log('Failed:', request.failure().errorText, request.url());
|
||||
});
|
||||
|
||||
// Get response body
|
||||
page.on('response', async (response) => {
|
||||
if (response.url().includes('/api/data')) {
|
||||
const json = await response.json();
|
||||
console.log('API Data:', json);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Device Emulation
|
||||
|
||||
### Predefined Devices
|
||||
|
||||
```javascript
|
||||
import { devices } from 'puppeteer';
|
||||
|
||||
// Emulate iPhone
|
||||
const iPhone = devices['iPhone 13 Pro'];
|
||||
await page.emulate(iPhone);
|
||||
|
||||
// Common devices
|
||||
const iPad = devices['iPad Pro'];
|
||||
const pixel = devices['Pixel 5'];
|
||||
const galaxy = devices['Galaxy S9+'];
|
||||
|
||||
// Navigate after emulation
|
||||
await page.goto('https://example.com');
|
||||
```
|
||||
|
||||
### Custom Device
|
||||
|
||||
```javascript
|
||||
await page.emulate({
|
||||
viewport: {
|
||||
width: 375,
|
||||
height: 812,
|
||||
deviceScaleFactor: 3,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
isLandscape: false
|
||||
},
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)...'
|
||||
});
|
||||
```
|
||||
|
||||
### Viewport Only
|
||||
|
||||
```javascript
|
||||
await page.setViewport({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
deviceScaleFactor: 1
|
||||
});
|
||||
```
|
||||
|
||||
### Geolocation
|
||||
|
||||
```javascript
|
||||
// Set geolocation
|
||||
await page.setGeolocation({
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
accuracy: 100
|
||||
});
|
||||
|
||||
// Grant permissions
|
||||
const context = browser.defaultBrowserContext();
|
||||
await context.overridePermissions('https://example.com', ['geolocation']);
|
||||
```
|
||||
|
||||
### Timezone & Locale
|
||||
|
||||
```javascript
|
||||
// Set timezone
|
||||
await page.emulateTimezone('America/New_York');
|
||||
|
||||
// Set locale
|
||||
await page.emulateMediaType('screen');
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
get: () => 'en-US'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### CPU & Network Throttling
|
||||
|
||||
```javascript
|
||||
// CPU throttling
|
||||
const client = await page.createCDPSession();
|
||||
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
|
||||
|
||||
// Network throttling
|
||||
await page.emulateNetworkConditions({
|
||||
offline: false,
|
||||
downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
|
||||
uploadThroughput: 750 * 1024 / 8, // 750 Kbps
|
||||
latency: 40 // 40ms RTT
|
||||
});
|
||||
|
||||
// Predefined profiles
|
||||
await page.emulateNetworkConditions(
|
||||
puppeteer.networkConditions['Fast 3G']
|
||||
);
|
||||
|
||||
// Disable throttling
|
||||
await page.emulateNetworkConditions({
|
||||
offline: false,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: -1,
|
||||
latency: 0
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
```javascript
|
||||
// Get metrics
|
||||
const metrics = await page.metrics();
|
||||
console.log(metrics);
|
||||
// {
|
||||
// Timestamp, Documents, Frames, JSEventListeners,
|
||||
// Nodes, LayoutCount, RecalcStyleCount,
|
||||
// LayoutDuration, RecalcStyleDuration,
|
||||
// ScriptDuration, TaskDuration,
|
||||
// JSHeapUsedSize, JSHeapTotalSize
|
||||
// }
|
||||
```
|
||||
|
||||
### Performance Tracing
|
||||
|
||||
```javascript
|
||||
// Start tracing
|
||||
await page.tracing.start({
|
||||
path: 'trace.json',
|
||||
categories: [
|
||||
'devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline'
|
||||
]
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto('https://example.com');
|
||||
|
||||
// Stop tracing
|
||||
await page.tracing.stop();
|
||||
|
||||
// Analyze trace in chrome://tracing
|
||||
```
|
||||
|
||||
### Coverage (Code Usage)
|
||||
|
||||
```javascript
|
||||
// Start JS coverage
|
||||
await page.coverage.startJSCoverage();
|
||||
|
||||
// Start CSS coverage
|
||||
await page.coverage.startCSSCoverage();
|
||||
|
||||
// Navigate
|
||||
await page.goto('https://example.com');
|
||||
|
||||
// Stop and get coverage
|
||||
const jsCoverage = await page.coverage.stopJSCoverage();
|
||||
const cssCoverage = await page.coverage.stopCSSCoverage();
|
||||
|
||||
// Calculate unused bytes
|
||||
let totalBytes = 0;
|
||||
let usedBytes = 0;
|
||||
for (const entry of [...jsCoverage, ...cssCoverage]) {
|
||||
totalBytes += entry.text.length;
|
||||
for (const range of entry.ranges) {
|
||||
usedBytes += range.end - range.start - 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Used: ${usedBytes / totalBytes * 100}%`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Wait for Elements
|
||||
|
||||
```javascript
|
||||
// Wait for selector
|
||||
await page.waitForSelector('.element', {
|
||||
visible: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
// Wait for XPath
|
||||
await page.waitForXPath('//button[text()="Submit"]');
|
||||
|
||||
// Wait for function
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector('.loading') === null,
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
// Wait for timeout
|
||||
await page.waitForTimeout(2000);
|
||||
```
|
||||
|
||||
### Handle Dialogs
|
||||
|
||||
```javascript
|
||||
// Alert, confirm, prompt
|
||||
page.on('dialog', async (dialog) => {
|
||||
console.log(dialog.type(), dialog.message());
|
||||
|
||||
// Accept
|
||||
await dialog.accept();
|
||||
// or reject
|
||||
// await dialog.dismiss();
|
||||
// or provide input for prompt
|
||||
// await dialog.accept('input text');
|
||||
});
|
||||
```
|
||||
|
||||
### Handle Downloads
|
||||
|
||||
```javascript
|
||||
// Set download path
|
||||
const client = await page.createCDPSession();
|
||||
await client.send('Page.setDownloadBehavior', {
|
||||
behavior: 'allow',
|
||||
downloadPath: '/path/to/downloads'
|
||||
});
|
||||
|
||||
// Trigger download
|
||||
await page.click('a[download]');
|
||||
```
|
||||
|
||||
### Multiple Pages (Tabs)
|
||||
|
||||
```javascript
|
||||
// Listen for new pages
|
||||
browser.on('targetcreated', async (target) => {
|
||||
if (target.type() === 'page') {
|
||||
const newPage = await target.page();
|
||||
console.log('New page opened:', newPage.url());
|
||||
}
|
||||
});
|
||||
|
||||
// Click link that opens new tab
|
||||
const [newPage] = await Promise.all([
|
||||
new Promise(resolve => browser.once('targetcreated', target => resolve(target.page()))),
|
||||
page.click('a[target="_blank"]')
|
||||
]);
|
||||
|
||||
console.log('New page URL:', newPage.url());
|
||||
```
|
||||
|
||||
### Frames (iframes)
|
||||
|
||||
```javascript
|
||||
// Get all frames
|
||||
const frames = page.frames();
|
||||
|
||||
// Find frame by name
|
||||
const frame = page.frames().find(f => f.name() === 'myframe');
|
||||
|
||||
// Find frame by URL
|
||||
const frame = page.frames().find(f => f.url().includes('example.com'));
|
||||
|
||||
// Main frame
|
||||
const mainFrame = page.mainFrame();
|
||||
|
||||
// Interact with frame
|
||||
await frame.click('.button');
|
||||
await frame.type('#input', 'text');
|
||||
```
|
||||
|
||||
### Infinite Scroll
|
||||
|
||||
```javascript
|
||||
async function autoScroll(page) {
|
||||
await page.evaluate(async () => {
|
||||
await new Promise((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 100;
|
||||
const timer = setInterval(() => {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
window.scrollBy(0, distance);
|
||||
totalHeight += distance;
|
||||
|
||||
if (totalHeight >= scrollHeight) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await autoScroll(page);
|
||||
```
|
||||
|
||||
### Cookies
|
||||
|
||||
```javascript
|
||||
// Get cookies
|
||||
const cookies = await page.cookies();
|
||||
|
||||
// Set cookies
|
||||
await page.setCookie({
|
||||
name: 'session',
|
||||
value: 'abc123',
|
||||
domain: 'example.com',
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Strict'
|
||||
});
|
||||
|
||||
// Delete cookies
|
||||
await page.deleteCookie({ name: 'session' });
|
||||
```
|
||||
|
||||
### Local Storage
|
||||
|
||||
```javascript
|
||||
// Set localStorage
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('key', 'value');
|
||||
});
|
||||
|
||||
// Get localStorage
|
||||
const value = await page.evaluate(() => {
|
||||
return localStorage.getItem('key');
|
||||
});
|
||||
|
||||
// Clear localStorage
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
await page.goto('https://example.com', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 30000
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.name === 'TimeoutError') {
|
||||
console.error('Page load timeout');
|
||||
} else {
|
||||
console.error('Navigation failed:', error);
|
||||
}
|
||||
|
||||
// Take screenshot on error
|
||||
await page.screenshot({ path: 'error.png' });
|
||||
}
|
||||
```
|
||||
|
||||
### Stealth Mode (Avoid Detection)
|
||||
|
||||
```javascript
|
||||
// Hide automation indicators
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
// Override navigator.webdriver
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => false
|
||||
});
|
||||
|
||||
// Mock chrome object
|
||||
window.chrome = {
|
||||
runtime: {}
|
||||
};
|
||||
|
||||
// Mock permissions
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications' ?
|
||||
Promise.resolve({ state: 'granted' }) :
|
||||
originalQuery(parameters)
|
||||
);
|
||||
});
|
||||
|
||||
// Set realistic user agent
|
||||
await page.setUserAgent(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Take Screenshots on Error
|
||||
|
||||
```javascript
|
||||
page.on('pageerror', async (error) => {
|
||||
console.error('Page error:', error);
|
||||
await page.screenshot({ path: `error-${Date.now()}.png` });
|
||||
});
|
||||
```
|
||||
|
||||
### Console Logging
|
||||
|
||||
```javascript
|
||||
// Forward console to Node
|
||||
page.on('console', (msg) => {
|
||||
console.log('PAGE LOG:', msg.text());
|
||||
});
|
||||
```
|
||||
|
||||
### Slow Down Execution
|
||||
|
||||
```javascript
|
||||
const browser = await puppeteer.launch({
|
||||
slowMo: 250 // 250ms delay between actions
|
||||
});
|
||||
```
|
||||
|
||||
### Keep Browser Open
|
||||
|
||||
```javascript
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
devtools: true
|
||||
});
|
||||
|
||||
// Prevent auto-close
|
||||
await page.evaluate(() => debugger);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always close browser:** Use try/finally or process cleanup
|
||||
2. **Wait appropriately:** Use waitForSelector, not setTimeout
|
||||
3. **Handle errors:** Wrap navigation in try/catch
|
||||
4. **Optimize selectors:** Use specific selectors for reliability
|
||||
5. **Avoid race conditions:** Wait for navigation after clicks
|
||||
6. **Reuse pages:** Don't create new pages unnecessarily
|
||||
7. **Set timeouts:** Always specify reasonable timeouts
|
||||
8. **Clean up:** Close unused pages and contexts
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Puppeteer Documentation](https://pptr.dev/)
|
||||
- [Puppeteer API](https://pptr.dev/api)
|
||||
- [Puppeteer Examples](https://github.com/puppeteer/puppeteer/tree/main/examples)
|
||||
- [Awesome Puppeteer](https://github.com/transitive-bullshit/awesome-puppeteer)
|
||||
213
skills/chrome-devtools/scripts/README.md
Normal file
213
skills/chrome-devtools/scripts/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Chrome DevTools Scripts
|
||||
|
||||
CLI scripts for browser automation using Puppeteer.
|
||||
|
||||
**CRITICAL**: Always check `pwd` before running scripts.
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Install
|
||||
|
||||
```bash
|
||||
pwd # Should show current working directory
|
||||
cd .claude/skills/chrome-devtools/scripts
|
||||
./install.sh # Auto-checks dependencies and installs
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
|
||||
**Linux/WSL** - Install system dependencies first:
|
||||
```bash
|
||||
./install-deps.sh # Auto-detects OS (Ubuntu, Debian, Fedora, etc.)
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1
|
||||
```
|
||||
|
||||
**All platforms** - Install Node dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
**CRITICAL**: Always check `pwd` before running scripts.
|
||||
|
||||
### navigate.js
|
||||
Navigate to a URL.
|
||||
|
||||
```bash
|
||||
node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
|
||||
```
|
||||
|
||||
### screenshot.js
|
||||
Take a screenshot with automatic compression.
|
||||
|
||||
**Important**: Always save screenshots to `./docs/screenshots` directory.
|
||||
|
||||
```bash
|
||||
node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
|
||||
```
|
||||
|
||||
**Automatic Compression**: Screenshots >5MB are automatically compressed using ImageMagick to ensure compatibility with Gemini API and Claude Code. Install ImageMagick for this feature:
|
||||
- macOS: `brew install imagemagick`
|
||||
- Linux: `sudo apt-get install imagemagick`
|
||||
|
||||
Options:
|
||||
- `--max-size N` - Custom size threshold in MB (default: 5)
|
||||
- `--no-compress` - Disable automatic compression
|
||||
- `--format png|jpeg` - Output format (default: png)
|
||||
- `--quality N` - JPEG quality 0-100 (default: auto)
|
||||
|
||||
### click.js
|
||||
Click an element.
|
||||
|
||||
```bash
|
||||
node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
|
||||
```
|
||||
|
||||
### fill.js
|
||||
Fill form fields.
|
||||
|
||||
```bash
|
||||
node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]
|
||||
```
|
||||
|
||||
### evaluate.js
|
||||
Execute JavaScript in page context.
|
||||
|
||||
```bash
|
||||
node evaluate.js --script "document.title" [--url https://example.com]
|
||||
```
|
||||
|
||||
### snapshot.js
|
||||
Get DOM snapshot with interactive elements.
|
||||
|
||||
```bash
|
||||
node snapshot.js [--url https://example.com] [--output snapshot.json]
|
||||
```
|
||||
|
||||
### console.js
|
||||
Monitor console messages.
|
||||
|
||||
```bash
|
||||
node console.js --url https://example.com [--types error,warn] [--duration 5000]
|
||||
```
|
||||
|
||||
### network.js
|
||||
Monitor network requests.
|
||||
|
||||
```bash
|
||||
node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
|
||||
```
|
||||
|
||||
### performance.js
|
||||
Measure performance metrics and record trace.
|
||||
|
||||
```bash
|
||||
node performance.js --url https://example.com [--trace trace.json] [--metrics] [--resources true]
|
||||
```
|
||||
|
||||
## Common Options
|
||||
|
||||
- `--headless false` - Show browser window
|
||||
- `--close false` - Keep browser open
|
||||
- `--timeout 30000` - Set timeout in milliseconds
|
||||
- `--wait-until networkidle2` - Wait strategy (load, domcontentloaded, networkidle0, networkidle2)
|
||||
|
||||
## Selector Support
|
||||
|
||||
Scripts that accept `--selector` (click.js, fill.js, screenshot.js) support both **CSS** and **XPath** selectors.
|
||||
|
||||
### CSS Selectors (Default)
|
||||
|
||||
```bash
|
||||
# Element tag
|
||||
node click.js --selector "button" --url https://example.com
|
||||
|
||||
# Class selector
|
||||
node click.js --selector ".btn-submit" --url https://example.com
|
||||
|
||||
# ID selector
|
||||
node fill.js --selector "#email" --value "user@example.com" --url https://example.com
|
||||
|
||||
# Attribute selector
|
||||
node click.js --selector 'button[type="submit"]' --url https://example.com
|
||||
|
||||
# Complex selector
|
||||
node screenshot.js --selector "div.container > button.btn-primary" --output btn.png
|
||||
```
|
||||
|
||||
### XPath Selectors
|
||||
|
||||
XPath selectors start with `/` or `(//` and are automatically detected:
|
||||
|
||||
```bash
|
||||
# Text matching - exact
|
||||
node click.js --selector '//button[text()="Submit"]' --url https://example.com
|
||||
|
||||
# Text matching - contains
|
||||
node click.js --selector '//button[contains(text(),"Submit")]' --url https://example.com
|
||||
|
||||
# Attribute matching
|
||||
node fill.js --selector '//input[@type="email"]' --value "user@example.com"
|
||||
|
||||
# Multiple conditions
|
||||
node click.js --selector '//button[@type="submit" and contains(text(),"Save")]'
|
||||
|
||||
# Descendant selection
|
||||
node screenshot.js --selector '//div[@class="modal"]//button[@class="close"]' --output modal.png
|
||||
|
||||
# Nth element
|
||||
node click.js --selector '(//button)[2]' # Second button on page
|
||||
```
|
||||
|
||||
### Discovering Selectors
|
||||
|
||||
Use `snapshot.js` to discover correct selectors:
|
||||
|
||||
```bash
|
||||
# Get all interactive elements
|
||||
node snapshot.js --url https://example.com | jq '.elements[]'
|
||||
|
||||
# Find buttons
|
||||
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
|
||||
|
||||
# Find inputs
|
||||
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="INPUT")'
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
XPath selectors are validated to prevent injection attacks. The following patterns are blocked:
|
||||
- `javascript:`
|
||||
- `<script`
|
||||
- `onerror=`, `onload=`, `onclick=`
|
||||
- `eval(`, `Function(`, `constructor(`
|
||||
|
||||
Selectors exceeding 1000 characters are rejected (DoS prevention).
|
||||
|
||||
## Output Format
|
||||
|
||||
All scripts output JSON to stdout:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"url": "https://example.com",
|
||||
"title": "Example Domain",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Errors are output to stderr:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message",
|
||||
"stack": "..."
|
||||
}
|
||||
```
|
||||
210
skills/chrome-devtools/scripts/__tests__/selector.test.js
Normal file
210
skills/chrome-devtools/scripts/__tests__/selector.test.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Tests for selector parsing library
|
||||
* Run with: node --test __tests__/selector.test.js
|
||||
*/
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { parseSelector } from '../lib/selector.js';
|
||||
|
||||
describe('parseSelector', () => {
|
||||
describe('CSS Selectors', () => {
|
||||
it('should detect simple CSS selectors', () => {
|
||||
const result = parseSelector('button');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, 'button');
|
||||
});
|
||||
|
||||
it('should detect class selectors', () => {
|
||||
const result = parseSelector('.btn-submit');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, '.btn-submit');
|
||||
});
|
||||
|
||||
it('should detect ID selectors', () => {
|
||||
const result = parseSelector('#email-input');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, '#email-input');
|
||||
});
|
||||
|
||||
it('should detect attribute selectors', () => {
|
||||
const result = parseSelector('button[type="submit"]');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, 'button[type="submit"]');
|
||||
});
|
||||
|
||||
it('should detect complex CSS selectors', () => {
|
||||
const result = parseSelector('div.container > button.btn-primary:hover');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
});
|
||||
});
|
||||
|
||||
describe('XPath Selectors', () => {
|
||||
it('should detect absolute XPath', () => {
|
||||
const result = parseSelector('/html/body/button');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
assert.strictEqual(result.selector, '/html/body/button');
|
||||
});
|
||||
|
||||
it('should detect relative XPath', () => {
|
||||
const result = parseSelector('//button');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
assert.strictEqual(result.selector, '//button');
|
||||
});
|
||||
|
||||
it('should detect XPath with text matching', () => {
|
||||
const result = parseSelector('//button[text()="Click Me"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should detect XPath with contains', () => {
|
||||
const result = parseSelector('//button[contains(text(),"Submit")]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should detect XPath with attributes', () => {
|
||||
const result = parseSelector('//input[@type="email"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should detect grouped XPath', () => {
|
||||
const result = parseSelector('(//button)[1]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Validation', () => {
|
||||
it('should block javascript: injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//button[@onclick="javascript:alert(1)"]'),
|
||||
/XPath injection detected.*javascript:/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block <script tag injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[contains(text(),"<script>alert(1)</script>")]'),
|
||||
/XPath injection detected.*<script/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block onerror= injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//img[@onerror="alert(1)"]'),
|
||||
/XPath injection detected.*onerror=/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block onload= injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//body[@onload="malicious()"]'),
|
||||
/XPath injection detected.*onload=/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block onclick= injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//a[@onclick="steal()"]'),
|
||||
/XPath injection detected.*onclick=/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block eval( injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[eval("malicious")]'),
|
||||
/XPath injection detected.*eval\(/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block Function( injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[Function("return 1")()]'),
|
||||
/XPath injection detected.*Function\(/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block constructor( injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[constructor("alert(1)")()]'),
|
||||
/XPath injection detected.*constructor\(/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should be case-insensitive for security checks', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[@ONERROR="alert(1)"]'),
|
||||
/XPath injection detected/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block extremely long selectors (DoS prevention)', () => {
|
||||
const longSelector = '//' + 'a'.repeat(1001);
|
||||
assert.throws(
|
||||
() => parseSelector(longSelector),
|
||||
/XPath selector too long/i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should throw on empty string', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(''),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on null', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(null),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on undefined', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(undefined),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on non-string input', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(123),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle selectors with special characters', () => {
|
||||
const result = parseSelector('button[data-test="submit-form"]');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
});
|
||||
|
||||
it('should allow safe XPath with parentheses', () => {
|
||||
const result = parseSelector('//button[contains(text(),"Save")]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
// Should not throw
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-World Examples', () => {
|
||||
it('should handle common button selector', () => {
|
||||
const result = parseSelector('//button[contains(text(),"Submit")]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should handle complex form selector', () => {
|
||||
const result = parseSelector('//form[@id="login-form"]//input[@type="email"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should handle descendant selector', () => {
|
||||
const result = parseSelector('//div[@class="modal"]//button[@class="close"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should handle nth-child equivalent', () => {
|
||||
const result = parseSelector('(//li)[3]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
});
|
||||
});
|
||||
79
skills/chrome-devtools/scripts/click.js
Normal file
79
skills/chrome-devtools/scripts/click.js
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Click an element
|
||||
* Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
|
||||
* Supports both CSS and XPath selectors:
|
||||
* - CSS: node click.js --selector "button.submit"
|
||||
* - XPath: node click.js --selector "//button[contains(text(),'Submit')]"
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import { parseSelector, waitForElement, clickElement, enhanceError } from './lib/selector.js';
|
||||
|
||||
async function click() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.selector) {
|
||||
outputError(new Error('--selector is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse and validate selector
|
||||
const parsed = parseSelector(args.selector);
|
||||
|
||||
// Wait for element based on selector type
|
||||
await waitForElement(page, parsed, {
|
||||
visible: true,
|
||||
timeout: parseInt(args.timeout || '5000')
|
||||
});
|
||||
|
||||
// Set up navigation promise BEFORE clicking (in case click triggers immediate navigation)
|
||||
const navigationPromise = page.waitForNavigation({
|
||||
waitUntil: 'load',
|
||||
timeout: 5000
|
||||
}).catch(() => null); // Catch timeout - navigation may not occur
|
||||
|
||||
// Click element
|
||||
await clickElement(page, parsed);
|
||||
|
||||
// Wait for optional selector after click
|
||||
if (args['wait-for']) {
|
||||
await page.waitForSelector(args['wait-for'], {
|
||||
timeout: parseInt(args.timeout || '5000')
|
||||
});
|
||||
} else {
|
||||
// Wait for navigation to complete (or timeout if no navigation)
|
||||
await navigationPromise;
|
||||
}
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
url: page.url(),
|
||||
title: await page.title()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error message with troubleshooting tips
|
||||
const enhanced = enhanceError(error, args.selector);
|
||||
outputError(enhanced);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
click();
|
||||
75
skills/chrome-devtools/scripts/console.js
Normal file
75
skills/chrome-devtools/scripts/console.js
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Monitor console messages
|
||||
* Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
|
||||
async function monitorConsole() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
const messages = [];
|
||||
const filterTypes = args.types ? args.types.split(',') : null;
|
||||
|
||||
// Listen for console messages
|
||||
page.on('console', (msg) => {
|
||||
const type = msg.type();
|
||||
|
||||
if (!filterTypes || filterTypes.includes(type)) {
|
||||
messages.push({
|
||||
type: type,
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for page errors
|
||||
page.on('pageerror', (error) => {
|
||||
messages.push({
|
||||
type: 'pageerror',
|
||||
text: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
|
||||
// Wait for additional time if specified
|
||||
if (args.duration) {
|
||||
await new Promise(resolve => setTimeout(resolve, parseInt(args.duration)));
|
||||
}
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
url: page.url(),
|
||||
messageCount: messages.length,
|
||||
messages: messages
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
monitorConsole();
|
||||
49
skills/chrome-devtools/scripts/evaluate.js
Normal file
49
skills/chrome-devtools/scripts/evaluate.js
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Execute JavaScript in page context
|
||||
* Usage: node evaluate.js --script "document.title" [--url https://example.com]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
|
||||
async function evaluate() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.script) {
|
||||
outputError(new Error('--script is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await page.evaluate((script) => {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(script);
|
||||
}, args.script);
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
result: result,
|
||||
url: page.url()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
evaluate();
|
||||
72
skills/chrome-devtools/scripts/fill.js
Normal file
72
skills/chrome-devtools/scripts/fill.js
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fill form fields
|
||||
* Usage: node fill.js --selector "#input" --value "text" [--url https://example.com]
|
||||
* Supports both CSS and XPath selectors:
|
||||
* - CSS: node fill.js --selector "#email" --value "user@example.com"
|
||||
* - XPath: node fill.js --selector "//input[@type='email']" --value "user@example.com"
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import { parseSelector, waitForElement, typeIntoElement, enhanceError } from './lib/selector.js';
|
||||
|
||||
async function fill() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.selector) {
|
||||
outputError(new Error('--selector is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.value) {
|
||||
outputError(new Error('--value is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse and validate selector
|
||||
const parsed = parseSelector(args.selector);
|
||||
|
||||
// Wait for element based on selector type
|
||||
await waitForElement(page, parsed, {
|
||||
visible: true,
|
||||
timeout: parseInt(args.timeout || '5000')
|
||||
});
|
||||
|
||||
// Type into element
|
||||
await typeIntoElement(page, parsed, args.value, {
|
||||
clear: args.clear === 'true',
|
||||
delay: parseInt(args.delay || '0')
|
||||
});
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
selector: args.selector,
|
||||
value: args.value,
|
||||
url: page.url()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error message with troubleshooting tips
|
||||
const enhanced = enhanceError(error, args.selector);
|
||||
outputError(enhanced);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fill();
|
||||
181
skills/chrome-devtools/scripts/install-deps.sh
Normal file
181
skills/chrome-devtools/scripts/install-deps.sh
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
# System dependencies installation script for Chrome DevTools Agent Skill
|
||||
# This script installs required system libraries for running Chrome/Chromium
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Installing system dependencies for Chrome/Chromium..."
|
||||
echo ""
|
||||
|
||||
# Detect OS
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
else
|
||||
echo "❌ Cannot detect OS. This script supports Debian/Ubuntu-based systems."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
SUDO="sudo"
|
||||
echo "⚠️ This script requires root privileges to install system packages."
|
||||
echo " You may be prompted for your password."
|
||||
echo ""
|
||||
else
|
||||
SUDO=""
|
||||
fi
|
||||
|
||||
# Install dependencies based on OS
|
||||
case $OS in
|
||||
ubuntu|debian|pop)
|
||||
echo "Detected: $PRETTY_NAME"
|
||||
echo "Installing dependencies with apt..."
|
||||
echo ""
|
||||
|
||||
$SUDO apt-get update
|
||||
|
||||
# Install Chrome dependencies
|
||||
$SUDO apt-get install -y \
|
||||
ca-certificates \
|
||||
fonts-liberation \
|
||||
libasound2t64 \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libc6 \
|
||||
libcairo2 \
|
||||
libcups2 \
|
||||
libdbus-1-3 \
|
||||
libexpat1 \
|
||||
libfontconfig1 \
|
||||
libgbm1 \
|
||||
libgcc1 \
|
||||
libglib2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libstdc++6 \
|
||||
libx11-6 \
|
||||
libx11-xcb1 \
|
||||
libxcb1 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
lsb-release \
|
||||
wget \
|
||||
xdg-utils
|
||||
|
||||
echo ""
|
||||
echo "✅ System dependencies installed successfully!"
|
||||
;;
|
||||
|
||||
fedora|rhel|centos)
|
||||
echo "Detected: $PRETTY_NAME"
|
||||
echo "Installing dependencies with dnf/yum..."
|
||||
echo ""
|
||||
|
||||
# Try dnf first, fallback to yum
|
||||
if command -v dnf &> /dev/null; then
|
||||
PKG_MGR="dnf"
|
||||
else
|
||||
PKG_MGR="yum"
|
||||
fi
|
||||
|
||||
$SUDO $PKG_MGR install -y \
|
||||
alsa-lib \
|
||||
atk \
|
||||
at-spi2-atk \
|
||||
cairo \
|
||||
cups-libs \
|
||||
dbus-libs \
|
||||
expat \
|
||||
fontconfig \
|
||||
glib2 \
|
||||
gtk3 \
|
||||
libdrm \
|
||||
libgbm \
|
||||
libX11 \
|
||||
libxcb \
|
||||
libXcomposite \
|
||||
libXcursor \
|
||||
libXdamage \
|
||||
libXext \
|
||||
libXfixes \
|
||||
libXi \
|
||||
libxkbcommon \
|
||||
libXrandr \
|
||||
libXrender \
|
||||
libXScrnSaver \
|
||||
libXtst \
|
||||
mesa-libgbm \
|
||||
nspr \
|
||||
nss \
|
||||
pango
|
||||
|
||||
echo ""
|
||||
echo "✅ System dependencies installed successfully!"
|
||||
;;
|
||||
|
||||
arch|manjaro)
|
||||
echo "Detected: $PRETTY_NAME"
|
||||
echo "Installing dependencies with pacman..."
|
||||
echo ""
|
||||
|
||||
$SUDO pacman -Sy --noconfirm \
|
||||
alsa-lib \
|
||||
at-spi2-core \
|
||||
cairo \
|
||||
cups \
|
||||
dbus \
|
||||
expat \
|
||||
glib2 \
|
||||
gtk3 \
|
||||
libdrm \
|
||||
libx11 \
|
||||
libxcb \
|
||||
libxcomposite \
|
||||
libxcursor \
|
||||
libxdamage \
|
||||
libxext \
|
||||
libxfixes \
|
||||
libxi \
|
||||
libxkbcommon \
|
||||
libxrandr \
|
||||
libxrender \
|
||||
libxshmfence \
|
||||
libxss \
|
||||
libxtst \
|
||||
mesa \
|
||||
nspr \
|
||||
nss \
|
||||
pango
|
||||
|
||||
echo ""
|
||||
echo "✅ System dependencies installed successfully!"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "❌ Unsupported OS: $OS"
|
||||
echo " This script supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro"
|
||||
echo ""
|
||||
echo " Please install Chrome/Chromium dependencies manually for your OS."
|
||||
echo " See: https://pptr.dev/troubleshooting"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "📝 Next steps:"
|
||||
echo " 1. Run: cd $(dirname "$0")"
|
||||
echo " 2. Run: npm install"
|
||||
echo " 3. Test: node navigate.js --url https://example.com"
|
||||
echo ""
|
||||
83
skills/chrome-devtools/scripts/install.sh
Normal file
83
skills/chrome-devtools/scripts/install.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
# Installation script for Chrome DevTools Agent Skill
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Installing Chrome DevTools Agent Skill..."
|
||||
echo ""
|
||||
|
||||
# Check Node.js version
|
||||
echo "Checking Node.js version..."
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
echo "❌ Error: Node.js 18+ is required. Current version: $(node --version)"
|
||||
echo " Please upgrade Node.js: https://nodejs.org/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Node.js version: $(node --version)"
|
||||
echo ""
|
||||
|
||||
# Check for system dependencies (Linux only)
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
echo "Checking system dependencies (Linux)..."
|
||||
|
||||
# Check for critical Chrome dependencies
|
||||
MISSING_DEPS=()
|
||||
|
||||
if ! ldconfig -p | grep -q libnss3.so; then
|
||||
MISSING_DEPS+=("libnss3")
|
||||
fi
|
||||
|
||||
if ! ldconfig -p | grep -q libnspr4.so; then
|
||||
MISSING_DEPS+=("libnspr4")
|
||||
fi
|
||||
|
||||
if ! ldconfig -p | grep -q libgbm.so; then
|
||||
MISSING_DEPS+=("libgbm1")
|
||||
fi
|
||||
|
||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
||||
echo "⚠️ Missing system dependencies: ${MISSING_DEPS[*]}"
|
||||
echo ""
|
||||
echo " Chrome/Chromium requires system libraries to run."
|
||||
echo " Install them with:"
|
||||
echo ""
|
||||
echo " ./install-deps.sh"
|
||||
echo ""
|
||||
echo " Or manually:"
|
||||
echo " sudo apt-get install -y libnss3 libnspr4 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2"
|
||||
echo ""
|
||||
|
||||
read -p " Continue anyway? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Installation cancelled."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✓ System dependencies found"
|
||||
fi
|
||||
echo ""
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "Platform: macOS (no system dependencies needed)"
|
||||
echo ""
|
||||
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
echo "Platform: Windows (no system dependencies needed)"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install Node.js dependencies
|
||||
echo "Installing Node.js dependencies..."
|
||||
npm install
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "Test the installation:"
|
||||
echo " node navigate.js --url https://example.com"
|
||||
echo ""
|
||||
echo "For more information:"
|
||||
echo " cat README.md"
|
||||
echo ""
|
||||
46
skills/chrome-devtools/scripts/navigate.js
Normal file
46
skills/chrome-devtools/scripts/navigate.js
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Navigate to a URL
|
||||
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
|
||||
async function navigate() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
const options = {
|
||||
waitUntil: args['wait-until'] || 'networkidle2',
|
||||
timeout: parseInt(args.timeout || '30000')
|
||||
};
|
||||
|
||||
await page.goto(args.url, options);
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
title: await page.title()
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
navigate();
|
||||
102
skills/chrome-devtools/scripts/network.js
Normal file
102
skills/chrome-devtools/scripts/network.js
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Monitor network requests
|
||||
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
async function monitorNetwork() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
const requests = [];
|
||||
const filterTypes = args.types ? args.types.split(',').map(t => t.toLowerCase()) : null;
|
||||
|
||||
// Monitor requests
|
||||
page.on('request', (request) => {
|
||||
const resourceType = request.resourceType().toLowerCase();
|
||||
|
||||
if (!filterTypes || filterTypes.includes(resourceType)) {
|
||||
requests.push({
|
||||
id: request._requestId || requests.length,
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
resourceType: resourceType,
|
||||
headers: request.headers(),
|
||||
postData: request.postData(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor responses
|
||||
const responses = new Map();
|
||||
page.on('response', async (response) => {
|
||||
const request = response.request();
|
||||
const resourceType = request.resourceType().toLowerCase();
|
||||
|
||||
if (!filterTypes || filterTypes.includes(resourceType)) {
|
||||
try {
|
||||
responses.set(request._requestId || request.url(), {
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
headers: response.headers(),
|
||||
fromCache: response.fromCache(),
|
||||
timing: response.timing()
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore errors for some response types
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
|
||||
// Merge requests with responses
|
||||
const combined = requests.map(req => ({
|
||||
...req,
|
||||
response: responses.get(req.id) || responses.get(req.url) || null
|
||||
}));
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
requestCount: combined.length,
|
||||
requests: combined
|
||||
};
|
||||
|
||||
if (args.output) {
|
||||
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
|
||||
outputJSON({
|
||||
success: true,
|
||||
output: args.output,
|
||||
requestCount: combined.length
|
||||
});
|
||||
} else {
|
||||
outputJSON(result);
|
||||
}
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
monitorNetwork();
|
||||
15
skills/chrome-devtools/scripts/package.json
Normal file
15
skills/chrome-devtools/scripts/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "chrome-devtools-scripts",
|
||||
"version": "1.0.0",
|
||||
"description": "Browser automation scripts for Chrome DevTools Agent Skill",
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"puppeteer": "^24.15.0",
|
||||
"debug": "^4.4.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
145
skills/chrome-devtools/scripts/performance.js
Normal file
145
skills/chrome-devtools/scripts/performance.js
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Measure performance metrics and record trace
|
||||
* Usage: node performance.js --url https://example.com [--trace trace.json] [--metrics]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
async function measurePerformance() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Start tracing if requested
|
||||
if (args.trace) {
|
||||
await page.tracing.start({
|
||||
path: args.trace,
|
||||
categories: [
|
||||
'devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline.frame'
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate
|
||||
await page.goto(args.url, {
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
// Stop tracing
|
||||
if (args.trace) {
|
||||
await page.tracing.stop();
|
||||
}
|
||||
|
||||
// Get performance metrics
|
||||
const metrics = await page.metrics();
|
||||
|
||||
// Get Core Web Vitals
|
||||
const vitals = await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const vitals = {
|
||||
LCP: null,
|
||||
FID: null,
|
||||
CLS: 0,
|
||||
FCP: null,
|
||||
TTFB: null
|
||||
};
|
||||
|
||||
// LCP
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
|
||||
}
|
||||
}).observe({ entryTypes: ['largest-contentful-paint'], buffered: true });
|
||||
} catch (e) {}
|
||||
|
||||
// CLS
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
vitals.CLS += entry.value;
|
||||
}
|
||||
});
|
||||
}).observe({ entryTypes: ['layout-shift'], buffered: true });
|
||||
} catch (e) {}
|
||||
|
||||
// FCP
|
||||
try {
|
||||
const paintEntries = performance.getEntriesByType('paint');
|
||||
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
|
||||
if (fcpEntry) {
|
||||
vitals.FCP = fcpEntry.startTime;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// TTFB
|
||||
try {
|
||||
const [navigationEntry] = performance.getEntriesByType('navigation');
|
||||
if (navigationEntry) {
|
||||
vitals.TTFB = navigationEntry.responseStart - navigationEntry.requestStart;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Wait a bit for metrics to stabilize
|
||||
setTimeout(() => resolve(vitals), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// Get resource timing
|
||||
const resources = await page.evaluate(() => {
|
||||
return performance.getEntriesByType('resource').map(r => ({
|
||||
name: r.name,
|
||||
type: r.initiatorType,
|
||||
duration: r.duration,
|
||||
size: r.transferSize,
|
||||
startTime: r.startTime
|
||||
}));
|
||||
});
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
metrics: {
|
||||
...metrics,
|
||||
JSHeapUsedSizeMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
|
||||
JSHeapTotalSizeMB: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2)
|
||||
},
|
||||
vitals: vitals,
|
||||
resources: {
|
||||
count: resources.length,
|
||||
totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
|
||||
items: args.resources === 'true' ? resources : undefined
|
||||
}
|
||||
};
|
||||
|
||||
if (args.trace) {
|
||||
result.trace = args.trace;
|
||||
}
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
measurePerformance();
|
||||
180
skills/chrome-devtools/scripts/screenshot.js
Normal file
180
skills/chrome-devtools/scripts/screenshot.js
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Take a screenshot
|
||||
* Usage: node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
|
||||
* Supports both CSS and XPath selectors:
|
||||
* - CSS: node screenshot.js --selector ".main-content" --output page.png
|
||||
* - XPath: node screenshot.js --selector "//div[@class='main-content']" --output page.png
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import { parseSelector, getElement, enhanceError } from './lib/selector.js';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* Compress image using ImageMagick if it exceeds max size
|
||||
* @param {string} filePath - Path to the image file
|
||||
* @param {number} maxSizeMB - Maximum file size in MB (default: 5)
|
||||
* @returns {Promise<{compressed: boolean, originalSize: number, finalSize: number}>}
|
||||
*/
|
||||
async function compressImageIfNeeded(filePath, maxSizeMB = 5) {
|
||||
const stats = await fs.stat(filePath);
|
||||
const originalSize = stats.size;
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||
|
||||
if (originalSize <= maxSizeBytes) {
|
||||
return { compressed: false, originalSize, finalSize: originalSize };
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if ImageMagick is available
|
||||
try {
|
||||
execSync('magick -version', { stdio: 'pipe' });
|
||||
} catch {
|
||||
try {
|
||||
execSync('convert -version', { stdio: 'pipe' });
|
||||
} catch {
|
||||
console.error('Warning: ImageMagick not found. Install it to enable automatic compression.');
|
||||
return { compressed: false, originalSize, finalSize: originalSize };
|
||||
}
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const tempPath = filePath.replace(ext, `.temp${ext}`);
|
||||
|
||||
// Determine compression strategy based on file type
|
||||
let compressionCmd;
|
||||
if (ext === '.png') {
|
||||
// For PNG: resize and compress with quality
|
||||
compressionCmd = `magick "${filePath}" -strip -resize 90% -quality 85 "${tempPath}"`;
|
||||
} else if (ext === '.jpg' || ext === '.jpeg') {
|
||||
// For JPEG: compress with quality and progressive
|
||||
compressionCmd = `magick "${filePath}" -strip -quality 80 -interlace Plane "${tempPath}"`;
|
||||
} else {
|
||||
// For other formats: convert to JPEG with compression
|
||||
compressionCmd = `magick "${filePath}" -strip -quality 80 "${tempPath.replace(ext, '.jpg')}"`;
|
||||
}
|
||||
|
||||
// Try compression
|
||||
execSync(compressionCmd, { stdio: 'pipe' });
|
||||
|
||||
const compressedStats = await fs.stat(tempPath);
|
||||
const compressedSize = compressedStats.size;
|
||||
|
||||
// If still too large, try more aggressive compression
|
||||
if (compressedSize > maxSizeBytes) {
|
||||
const finalPath = filePath.replace(ext, `.final${ext}`);
|
||||
let aggressiveCmd;
|
||||
|
||||
if (ext === '.png') {
|
||||
aggressiveCmd = `magick "${tempPath}" -strip -resize 75% -quality 70 "${finalPath}"`;
|
||||
} else {
|
||||
aggressiveCmd = `magick "${tempPath}" -strip -quality 60 -sampling-factor 4:2:0 "${finalPath}"`;
|
||||
}
|
||||
|
||||
execSync(aggressiveCmd, { stdio: 'pipe' });
|
||||
await fs.unlink(tempPath);
|
||||
await fs.rename(finalPath, filePath);
|
||||
} else {
|
||||
await fs.rename(tempPath, filePath);
|
||||
}
|
||||
|
||||
const finalStats = await fs.stat(filePath);
|
||||
return { compressed: true, originalSize, finalSize: finalStats.size };
|
||||
} catch (error) {
|
||||
console.error('Compression error:', error.message);
|
||||
// If compression fails, keep original file
|
||||
try {
|
||||
const tempPath = filePath.replace(path.extname(filePath), '.temp' + path.extname(filePath));
|
||||
await fs.unlink(tempPath).catch(() => {});
|
||||
} catch {}
|
||||
return { compressed: false, originalSize, finalSize: originalSize };
|
||||
}
|
||||
}
|
||||
|
||||
async function screenshot() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.output) {
|
||||
outputError(new Error('--output is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
const screenshotOptions = {
|
||||
path: args.output,
|
||||
type: args.format || 'png',
|
||||
fullPage: args['full-page'] === 'true'
|
||||
};
|
||||
|
||||
if (args.quality) {
|
||||
screenshotOptions.quality = parseInt(args.quality);
|
||||
}
|
||||
|
||||
let buffer;
|
||||
if (args.selector) {
|
||||
// Parse and validate selector
|
||||
const parsed = parseSelector(args.selector);
|
||||
|
||||
// Get element based on selector type
|
||||
const element = await getElement(page, parsed);
|
||||
if (!element) {
|
||||
throw new Error(`Element not found: ${args.selector}`);
|
||||
}
|
||||
buffer = await element.screenshot(screenshotOptions);
|
||||
} else {
|
||||
buffer = await page.screenshot(screenshotOptions);
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
output: path.resolve(args.output),
|
||||
size: buffer.length,
|
||||
url: page.url()
|
||||
};
|
||||
|
||||
// Compress image if needed (unless --no-compress flag is set)
|
||||
if (args['no-compress'] !== 'true') {
|
||||
const maxSize = args['max-size'] ? parseFloat(args['max-size']) : 5;
|
||||
const compressionResult = await compressImageIfNeeded(args.output, maxSize);
|
||||
|
||||
if (compressionResult.compressed) {
|
||||
result.compressed = true;
|
||||
result.originalSize = compressionResult.originalSize;
|
||||
result.size = compressionResult.finalSize;
|
||||
result.compressionRatio = ((1 - compressionResult.finalSize / compressionResult.originalSize) * 100).toFixed(2) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error message if selector-related
|
||||
if (args.selector) {
|
||||
const enhanced = enhanceError(error, args.selector);
|
||||
outputError(enhanced);
|
||||
} else {
|
||||
outputError(error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
screenshot();
|
||||
131
skills/chrome-devtools/scripts/snapshot.js
Normal file
131
skills/chrome-devtools/scripts/snapshot.js
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Get DOM snapshot with selectors
|
||||
* Usage: node snapshot.js [--url https://example.com] [--output snapshot.json]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
async function snapshot() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
// Get interactive elements with metadata
|
||||
const elements = await page.evaluate(() => {
|
||||
const interactiveSelectors = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'input',
|
||||
'textarea',
|
||||
'select',
|
||||
'[onclick]',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[contenteditable]'
|
||||
];
|
||||
|
||||
const elements = [];
|
||||
const selector = interactiveSelectors.join(', ');
|
||||
const nodes = document.querySelectorAll(selector);
|
||||
|
||||
nodes.forEach((el, index) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
// Generate unique selector
|
||||
let uniqueSelector = '';
|
||||
if (el.id) {
|
||||
uniqueSelector = `#${el.id}`;
|
||||
} else if (el.className) {
|
||||
const classes = Array.from(el.classList).join('.');
|
||||
uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`;
|
||||
} else {
|
||||
uniqueSelector = el.tagName.toLowerCase();
|
||||
}
|
||||
|
||||
elements.push({
|
||||
index: index,
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
type: el.type || null,
|
||||
id: el.id || null,
|
||||
className: el.className || null,
|
||||
name: el.name || null,
|
||||
value: el.value || null,
|
||||
text: el.textContent?.trim().substring(0, 100) || null,
|
||||
href: el.href || null,
|
||||
selector: uniqueSelector,
|
||||
xpath: getXPath(el),
|
||||
visible: rect.width > 0 && rect.height > 0,
|
||||
position: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getXPath(element) {
|
||||
if (element.id) {
|
||||
return `//*[@id="${element.id}"]`;
|
||||
}
|
||||
if (element === document.body) {
|
||||
return '/html/body';
|
||||
}
|
||||
let ix = 0;
|
||||
const siblings = element.parentNode?.childNodes || [];
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
const sibling = siblings[i];
|
||||
if (sibling === element) {
|
||||
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
|
||||
}
|
||||
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
||||
ix++;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return elements;
|
||||
});
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
title: await page.title(),
|
||||
elementCount: elements.length,
|
||||
elements: elements
|
||||
};
|
||||
|
||||
if (args.output) {
|
||||
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
|
||||
outputJSON({
|
||||
success: true,
|
||||
output: args.output,
|
||||
elementCount: elements.length
|
||||
});
|
||||
} else {
|
||||
outputJSON(result);
|
||||
}
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
snapshot();
|
||||
181
skills/claude-code/SKILL.md
Normal file
181
skills/claude-code/SKILL.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Claude Code Expert
|
||||
|
||||
Claude Code is Anthropic's agentic coding tool that lives in the terminal and helps turn ideas into code faster. It combines autonomous planning, execution, and validation with extensibility through skills, plugins, MCP servers, and hooks.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when users need help with:
|
||||
- Understanding Claude Code features and capabilities
|
||||
- Installation, setup, and authentication
|
||||
- Using slash commands for development workflows
|
||||
- Creating or managing Agent Skills
|
||||
- Configuring MCP servers for external tool integration
|
||||
- Setting up hooks and plugins
|
||||
- Troubleshooting Claude Code issues
|
||||
- Enterprise deployment (SSO, sandboxing, monitoring)
|
||||
- IDE integration (VS Code, JetBrains)
|
||||
- CI/CD integration (GitHub Actions, GitLab)
|
||||
- Advanced features (extended thinking, caching, checkpointing)
|
||||
- Cost tracking and optimization
|
||||
|
||||
**Activation examples:**
|
||||
- "How do I use Claude Code?"
|
||||
- "What slash commands are available?"
|
||||
- "How to set up MCP servers?"
|
||||
- "Create a new skill for X"
|
||||
- "Fix Claude Code authentication issues"
|
||||
- "Deploy Claude Code in enterprise environment"
|
||||
|
||||
## Core Architecture
|
||||
|
||||
**Subagents**: Specialized AI agents (planner, code-reviewer, tester, debugger, docs-manager, ui-ux-designer, database-admin, etc.)
|
||||
|
||||
**Agent Skills**: Modular capabilities with instructions, metadata, and resources that Claude uses automatically
|
||||
|
||||
**Slash Commands**: User-defined operations in `.claude/commands/` that expand to prompts
|
||||
|
||||
**Hooks**: Shell commands executing in response to events (pre/post-tool, user-prompt-submit)
|
||||
|
||||
**MCP Servers**: Model Context Protocol integrations connecting external tools and services
|
||||
|
||||
**Plugins**: Packaged collections of commands, skills, hooks, and MCP servers
|
||||
|
||||
## Quick Reference
|
||||
|
||||
Load these references when needed for detailed guidance:
|
||||
|
||||
### Getting Started
|
||||
- **Installation & Setup**: `references/getting-started.md`
|
||||
- Prerequisites, installation methods, authentication, first run
|
||||
|
||||
### Development Workflows
|
||||
- **Slash Commands**: `references/slash-commands.md`
|
||||
- Complete command catalog: /cook, /plan, /debug, /test, /fix:*, /docs:*, /git:*, /design:*, /content:*
|
||||
|
||||
- **Agent Skills**: `references/agent-skills.md`
|
||||
- Creating skills, skill.json format, best practices, API usage
|
||||
|
||||
### Integration & Extension
|
||||
- **MCP Integration**: `references/mcp-integration.md`
|
||||
- Configuration, common servers, remote servers
|
||||
|
||||
- **Hooks & Plugins**: `references/hooks-and-plugins.md`
|
||||
- Hook types, configuration, environment variables, plugin structure, installation
|
||||
|
||||
### Configuration & Settings
|
||||
- **Configuration**: `references/configuration.md`
|
||||
- Settings hierarchy, key settings, model configuration, output styles
|
||||
|
||||
### Enterprise & Production
|
||||
- **Enterprise Features**: `references/enterprise-features.md`
|
||||
- IAM, SSO, RBAC, sandboxing, audit logging, deployment options, monitoring
|
||||
|
||||
- **IDE Integration**: `references/ide-integration.md`
|
||||
- VS Code extension, JetBrains plugin setup and features
|
||||
|
||||
- **CI/CD Integration**: `references/cicd-integration.md`
|
||||
- GitHub Actions, GitLab CI/CD workflow examples
|
||||
|
||||
### Advanced Usage
|
||||
- **Advanced Features**: `references/advanced-features.md`
|
||||
- Extended thinking, prompt caching, checkpointing, memory management
|
||||
|
||||
- **Troubleshooting**: `references/troubleshooting.md`
|
||||
- Common issues, authentication failures, MCP problems, performance, debug mode
|
||||
|
||||
- **API Reference**: `references/api-reference.md`
|
||||
- Admin API, Messages API, Files API, Models API, Skills API
|
||||
|
||||
- **Best Practices**: `references/best-practices.md`
|
||||
- Project organization, security, performance, team collaboration, cost management
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Feature Implementation
|
||||
```bash
|
||||
/cook implement user authentication with JWT
|
||||
# Or plan first
|
||||
/plan implement payment integration with Stripe
|
||||
```
|
||||
|
||||
### Bug Fixing
|
||||
```bash
|
||||
/fix:fast the login button is not working
|
||||
/debug the API returns 500 errors intermittently
|
||||
/fix:types # Fix TypeScript errors
|
||||
```
|
||||
|
||||
### Code Review & Testing
|
||||
```bash
|
||||
claude "review my latest commit"
|
||||
/test
|
||||
/fix:test the user service tests are failing
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```bash
|
||||
/docs:init # Create initial documentation
|
||||
/docs:update # Update existing docs
|
||||
/docs:summarize # Summarize changes
|
||||
```
|
||||
|
||||
### Git Operations
|
||||
```bash
|
||||
/git:cm # Stage and commit
|
||||
/git:cp # Stage, commit, and push
|
||||
/git:pr feature-branch main # Create pull request
|
||||
```
|
||||
|
||||
### Design & Content
|
||||
```bash
|
||||
/design:fast create landing page for SaaS product
|
||||
/content:good write product description for new feature
|
||||
```
|
||||
|
||||
## Instructions for Claude
|
||||
|
||||
When responding to Claude Code questions:
|
||||
|
||||
1. **Identify the topic** from the user's question
|
||||
2. **Load relevant references** from the Quick Reference section above
|
||||
3. **Provide specific guidance** using information from loaded references
|
||||
4. **Include examples** when helpful
|
||||
5. **Reference documentation links** from llms.txt when appropriate
|
||||
|
||||
**Loading references:**
|
||||
- Read reference files only when needed for the specific question
|
||||
- Multiple references can be loaded for complex queries
|
||||
- Use grep patterns if searching within references
|
||||
|
||||
**For setup/installation questions:** Load `references/getting-started.md`
|
||||
|
||||
**For slash command questions:** Load `references/slash-commands.md`
|
||||
|
||||
**For skill creation:** Load `references/agent-skills.md`
|
||||
|
||||
**For MCP questions:** Load `references/mcp-integration.md`
|
||||
|
||||
**For hooks/plugins:** Load `references/hooks-and-plugins.md`
|
||||
|
||||
**For configuration:** Load `references/configuration.md`
|
||||
|
||||
**For enterprise deployment:** Load `references/enterprise-features.md`
|
||||
|
||||
**For IDE integration:** Load `references/ide-integration.md`
|
||||
|
||||
**For CI/CD:** Load `references/cicd-integration.md`
|
||||
|
||||
**For advanced features:** Load `references/advanced-features.md`
|
||||
|
||||
**For troubleshooting:** Load `references/troubleshooting.md`
|
||||
|
||||
**For API usage:** Load `references/api-reference.md`
|
||||
|
||||
**For best practices:** Load `references/best-practices.md`
|
||||
|
||||
**Documentation links:**
|
||||
- Main docs: https://docs.claude.com/claude-code
|
||||
- GitHub: https://github.com/anthropics/claude-code
|
||||
- Support: support.claude.com
|
||||
|
||||
Provide accurate, actionable guidance based on the loaded references and official documentation.
|
||||
135
skills/claude-code/llms.txt
Normal file
135
skills/claude-code/llms.txt
Normal file
@@ -0,0 +1,135 @@
|
||||
# Claude Code Documentation
|
||||
|
||||
## Overview
|
||||
Claude Code expert skill providing comprehensive guidance on features, setup, configuration, and troubleshooting.
|
||||
|
||||
## Core Topics
|
||||
|
||||
### Getting Started
|
||||
- Installation methods (npm, pip)
|
||||
- Authentication and API keys
|
||||
- First run and basic usage
|
||||
- See: references/getting-started.md
|
||||
|
||||
### Slash Commands
|
||||
- Development commands (/cook, /plan, /debug, /test)
|
||||
- Fix commands (/fix:fast, /fix:hard, /fix:types, /fix:test, /fix:ui, /fix:ci)
|
||||
- Documentation commands (/docs:init, /docs:update, /docs:summarize)
|
||||
- Git commands (/git:cm, /git:cp, /git:pr)
|
||||
- Design and content commands
|
||||
- See: references/slash-commands.md
|
||||
|
||||
### Agent Skills
|
||||
- Creating and managing skills
|
||||
- skill.md and skill.json structure
|
||||
- Resource types (scripts, references, assets)
|
||||
- API usage
|
||||
- See: references/agent-skills.md
|
||||
|
||||
### MCP Integration
|
||||
- MCP server configuration
|
||||
- Common servers (filesystem, GitHub, PostgreSQL, Brave Search, Puppeteer)
|
||||
- Remote MCP servers
|
||||
- Environment variables and security
|
||||
- See: references/mcp-integration.md
|
||||
|
||||
### Hooks and Plugins
|
||||
- Hook types (pre-tool, post-tool, user-prompt-submit)
|
||||
- Hook configuration and examples
|
||||
- Plugin structure and management
|
||||
- Creating and publishing plugins
|
||||
- See: references/hooks-and-plugins.md
|
||||
|
||||
### Configuration
|
||||
- Settings hierarchy (CLI flags, env vars, project, global)
|
||||
- Model configuration and aliases
|
||||
- Token and temperature settings
|
||||
- Sandboxing and memory management
|
||||
- Output styles
|
||||
- See: references/configuration.md
|
||||
|
||||
### Enterprise Features
|
||||
- Identity & Access Management (SSO, RBAC)
|
||||
- Security & Compliance (sandboxing, audit logging, certifications)
|
||||
- Deployment options (Bedrock, Vertex AI, self-hosted)
|
||||
- Monitoring & Analytics
|
||||
- Network configuration
|
||||
- See: references/enterprise-features.md
|
||||
|
||||
### IDE Integration
|
||||
- Visual Studio Code extension
|
||||
- JetBrains plugin
|
||||
- Features, configuration, keyboard shortcuts
|
||||
- See: references/ide-integration.md
|
||||
|
||||
### CI/CD Integration
|
||||
- GitHub Actions workflows
|
||||
- GitLab CI/CD pipelines
|
||||
- Automated testing and code review
|
||||
- See: references/cicd-integration.md
|
||||
|
||||
### Advanced Features
|
||||
- Extended thinking
|
||||
- Prompt caching
|
||||
- Checkpointing
|
||||
- Memory management
|
||||
- Context windows
|
||||
- See: references/advanced-features.md
|
||||
|
||||
### Troubleshooting
|
||||
- Authentication issues
|
||||
- Installation problems
|
||||
- Connection & network issues
|
||||
- MCP server problems
|
||||
- Performance issues
|
||||
- Tool execution errors
|
||||
- Debug mode
|
||||
- See: references/troubleshooting.md
|
||||
|
||||
### API Reference
|
||||
- Admin API (usage reports, cost reports, user management)
|
||||
- Messages API (create, stream, count tokens)
|
||||
- Files API (upload, list, download, delete)
|
||||
- Models API (list, get)
|
||||
- Skills API (create, list, update, delete)
|
||||
- Client SDKs (TypeScript, Python)
|
||||
- See: references/api-reference.md
|
||||
|
||||
### Best Practices
|
||||
- Project organization
|
||||
- Security (API keys, sandboxing, hooks, plugins)
|
||||
- Performance optimization
|
||||
- Team collaboration
|
||||
- Cost management
|
||||
- Development workflows
|
||||
- See: references/best-practices.md
|
||||
|
||||
## Documentation Links
|
||||
|
||||
### Official Documentation
|
||||
- Main docs: https://docs.claude.com/claude-code
|
||||
- API reference: https://docs.claude.com/api
|
||||
- Agent Skills: https://docs.claude.com/agents-and-tools/agent-skills
|
||||
- Models: https://docs.claude.com/about-claude/models
|
||||
|
||||
### Support
|
||||
- GitHub Issues: https://github.com/anthropics/claude-code/issues
|
||||
- Support Portal: support.claude.com
|
||||
- Community Discord: discord.gg/anthropic
|
||||
|
||||
## Reference Files
|
||||
|
||||
All detailed documentation is available in the references/ directory:
|
||||
- references/getting-started.md
|
||||
- references/slash-commands.md
|
||||
- references/agent-skills.md
|
||||
- references/mcp-integration.md
|
||||
- references/hooks-and-plugins.md
|
||||
- references/configuration.md
|
||||
- references/enterprise-features.md
|
||||
- references/ide-integration.md
|
||||
- references/cicd-integration.md
|
||||
- references/advanced-features.md
|
||||
- references/troubleshooting.md
|
||||
- references/api-reference.md
|
||||
- references/best-practices.md
|
||||
399
skills/claude-code/references/advanced-features.md
Normal file
399
skills/claude-code/references/advanced-features.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# Advanced Features
|
||||
|
||||
Extended thinking, prompt caching, checkpointing, and memory management in Claude Code.
|
||||
|
||||
## Extended Thinking
|
||||
|
||||
Deep reasoning for complex problems.
|
||||
|
||||
### Enable Extended Thinking
|
||||
|
||||
**Global configuration:**
|
||||
```bash
|
||||
claude config set thinking.enabled true
|
||||
claude config set thinking.budget 15000
|
||||
```
|
||||
|
||||
**Project settings (.claude/settings.json):**
|
||||
```json
|
||||
{
|
||||
"thinking": {
|
||||
"enabled": true,
|
||||
"budget": 10000,
|
||||
"mode": "auto"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Command-line flag:**
|
||||
```bash
|
||||
claude --thinking "architect microservices system"
|
||||
```
|
||||
|
||||
### Thinking Modes
|
||||
|
||||
**auto**: Claude decides when to use extended thinking
|
||||
**manual**: User explicitly requests thinking
|
||||
**disabled**: No extended thinking
|
||||
|
||||
```json
|
||||
{
|
||||
"thinking": {
|
||||
"mode": "auto",
|
||||
"budget": 10000,
|
||||
"minComplexity": 0.7
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Budget Control
|
||||
|
||||
Set token budget for thinking:
|
||||
|
||||
```json
|
||||
{
|
||||
"thinking": {
|
||||
"budget": 10000, // Max tokens for thinking
|
||||
"budgetPerRequest": 5000, // Per-request limit
|
||||
"adaptive": true // Adjust based on task complexity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Use Cases
|
||||
|
||||
- Architecture design
|
||||
- Complex algorithm development
|
||||
- System refactoring
|
||||
- Performance optimization
|
||||
- Security analysis
|
||||
- Bug investigation
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
claude --thinking "Design a distributed caching system with:
|
||||
- High availability
|
||||
- Consistency guarantees
|
||||
- Horizontal scalability
|
||||
- Fault tolerance"
|
||||
```
|
||||
|
||||
## Prompt Caching
|
||||
|
||||
Reduce costs by caching repeated context.
|
||||
|
||||
### Enable Caching
|
||||
|
||||
**API usage:**
|
||||
```typescript
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
system: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'You are a coding assistant...',
|
||||
cache_control: { type: 'ephemeral' }
|
||||
}
|
||||
],
|
||||
messages: [...]
|
||||
});
|
||||
```
|
||||
|
||||
**CLI configuration:**
|
||||
```json
|
||||
{
|
||||
"caching": {
|
||||
"enabled": true,
|
||||
"ttl": 300,
|
||||
"maxSize": "100MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Strategy
|
||||
|
||||
**What to cache:**
|
||||
- Large codebases
|
||||
- Documentation
|
||||
- API specifications
|
||||
- System prompts
|
||||
- Project context
|
||||
|
||||
**What not to cache:**
|
||||
- User queries
|
||||
- Dynamic content
|
||||
- Temporary data
|
||||
- Session-specific info
|
||||
|
||||
### Cache Control
|
||||
|
||||
```typescript
|
||||
// Cache large context
|
||||
{
|
||||
type: 'text',
|
||||
text: largeCodebase,
|
||||
cache_control: { type: 'ephemeral' }
|
||||
}
|
||||
|
||||
// Update without invalidating cache
|
||||
{
|
||||
type: 'text',
|
||||
text: newUserQuery
|
||||
// No cache_control = not cached
|
||||
}
|
||||
```
|
||||
|
||||
### Cost Savings
|
||||
|
||||
With caching:
|
||||
- First request: Full cost
|
||||
- Subsequent requests: ~90% discount on cached tokens
|
||||
- Cache TTL: 5 minutes
|
||||
|
||||
Example:
|
||||
```
|
||||
Without caching:
|
||||
Request 1: 10,000 tokens @ $3/M = $0.03
|
||||
Request 2: 10,000 tokens @ $3/M = $0.03
|
||||
Total: $0.06
|
||||
|
||||
With caching (8,000 tokens cached):
|
||||
Request 1: 10,000 tokens @ $3/M = $0.03
|
||||
Request 2: 2,000 new + 8,000 cached @ $0.30/M = $0.0024
|
||||
Total: $0.0324 (46% savings)
|
||||
```
|
||||
|
||||
## Checkpointing
|
||||
|
||||
Automatically track and rewind changes.
|
||||
|
||||
### Enable Checkpointing
|
||||
|
||||
```bash
|
||||
claude config set checkpointing.enabled true
|
||||
```
|
||||
|
||||
**Settings:**
|
||||
```json
|
||||
{
|
||||
"checkpointing": {
|
||||
"enabled": true,
|
||||
"autoSave": true,
|
||||
"interval": 300,
|
||||
"maxCheckpoints": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### View Checkpoints
|
||||
|
||||
```bash
|
||||
# List checkpoints
|
||||
claude checkpoint list
|
||||
|
||||
# View checkpoint details
|
||||
claude checkpoint show checkpoint-123
|
||||
```
|
||||
|
||||
### Restore Checkpoint
|
||||
|
||||
```bash
|
||||
# Restore to checkpoint
|
||||
claude checkpoint restore checkpoint-123
|
||||
|
||||
# Restore to time
|
||||
claude checkpoint restore --time "2025-11-06T10:00:00Z"
|
||||
|
||||
# Restore specific files
|
||||
claude checkpoint restore checkpoint-123 --files src/main.js
|
||||
```
|
||||
|
||||
### Create Manual Checkpoint
|
||||
|
||||
```bash
|
||||
# Create checkpoint with message
|
||||
claude checkpoint create "before refactoring auth module"
|
||||
|
||||
# Create at important moments
|
||||
claude checkpoint create "working state before experiment"
|
||||
```
|
||||
|
||||
### Checkpoint Strategies
|
||||
|
||||
**Auto-save checkpoints:**
|
||||
- Before major changes
|
||||
- After successful tests
|
||||
- Every N minutes
|
||||
- Before destructive operations
|
||||
|
||||
**Manual checkpoints:**
|
||||
- Before risky refactors
|
||||
- At working states
|
||||
- Before experiments
|
||||
- After milestones
|
||||
|
||||
### Example Workflow
|
||||
|
||||
```bash
|
||||
# Create checkpoint before risky change
|
||||
claude checkpoint create "before performance optimization"
|
||||
|
||||
# Make changes
|
||||
claude "optimize database queries for 10x performance"
|
||||
|
||||
# If something breaks
|
||||
claude checkpoint restore "before performance optimization"
|
||||
|
||||
# Or continue with improvements
|
||||
claude checkpoint create "performance optimization complete"
|
||||
```
|
||||
|
||||
## Memory Management
|
||||
|
||||
Control how Claude remembers context across sessions.
|
||||
|
||||
### Memory Locations
|
||||
|
||||
**global**: Share memory across all projects
|
||||
**project**: Project-specific memory
|
||||
**none**: Disable memory
|
||||
|
||||
```bash
|
||||
# Set memory location
|
||||
claude config set memory.location project
|
||||
|
||||
# Enable memory
|
||||
claude config set memory.enabled true
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"memory": {
|
||||
"enabled": true,
|
||||
"location": "project",
|
||||
"ttl": 86400,
|
||||
"maxSize": "10MB",
|
||||
"autoSummarize": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Operations
|
||||
|
||||
```bash
|
||||
# View stored memories
|
||||
claude memory list
|
||||
|
||||
# View specific memory
|
||||
claude memory show memory-123
|
||||
|
||||
# Clear all memories
|
||||
claude memory clear
|
||||
|
||||
# Clear old memories
|
||||
claude memory clear --older-than 7d
|
||||
|
||||
# Clear project memories
|
||||
claude memory clear --project
|
||||
```
|
||||
|
||||
### What Gets Remembered
|
||||
|
||||
**Automatically:**
|
||||
- Project structure
|
||||
- Coding patterns
|
||||
- Preferences
|
||||
- Common commands
|
||||
- File locations
|
||||
|
||||
**Explicitly stored:**
|
||||
- Important context
|
||||
- Design decisions
|
||||
- Architecture notes
|
||||
- Team conventions
|
||||
|
||||
### Memory Best Practices
|
||||
|
||||
**Project memory:**
|
||||
- Good for project-specific context
|
||||
- Shares across team members
|
||||
- Persists in `.claude/memory/`
|
||||
- Commit to version control (optional)
|
||||
|
||||
**Global memory:**
|
||||
- Personal preferences
|
||||
- General knowledge
|
||||
- Common patterns
|
||||
- Cross-project learnings
|
||||
|
||||
**Disable memory when:**
|
||||
- Working with sensitive data
|
||||
- One-off tasks
|
||||
- Testing/experimentation
|
||||
- Troubleshooting
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
# Remember project architecture
|
||||
claude "Remember: This project uses Clean Architecture with:
|
||||
- Domain layer (core business logic)
|
||||
- Application layer (use cases)
|
||||
- Infrastructure layer (external dependencies)
|
||||
- Presentation layer (API/UI)"
|
||||
|
||||
# Claude will recall this in future sessions
|
||||
claude "Add a new user registration feature"
|
||||
# Claude: "I'll implement this following the Clean Architecture..."
|
||||
```
|
||||
|
||||
## Context Windows
|
||||
|
||||
Manage large context effectively.
|
||||
|
||||
### Maximum Context
|
||||
|
||||
Model context limits:
|
||||
- Claude Sonnet: 200k tokens
|
||||
- Claude Opus: 200k tokens
|
||||
- Claude Haiku: 200k tokens
|
||||
|
||||
### Context Management
|
||||
|
||||
```json
|
||||
{
|
||||
"context": {
|
||||
"maxTokens": 200000,
|
||||
"autoTruncate": true,
|
||||
"prioritize": ["recent", "relevant"],
|
||||
"summarizeLong": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategies
|
||||
|
||||
**Summarization:**
|
||||
- Auto-summarize old context
|
||||
- Keep summaries of large files
|
||||
- Compress conversation history
|
||||
|
||||
**Prioritization:**
|
||||
- Recent messages first
|
||||
- Most relevant files
|
||||
- Explicit user priorities
|
||||
|
||||
**Chunking:**
|
||||
- Process large codebases in chunks
|
||||
- Incremental analysis
|
||||
- Parallel processing
|
||||
|
||||
## See Also
|
||||
|
||||
- Pricing: https://docs.claude.com/about-claude/pricing
|
||||
- Token counting: https://docs.claude.com/build-with-claude/token-counting
|
||||
- Best practices: `references/best-practices.md`
|
||||
- Configuration: `references/configuration.md`
|
||||
414
skills/claude-code/references/agent-skills.md
Normal file
414
skills/claude-code/references/agent-skills.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# Agent Skills
|
||||
|
||||
Create, manage, and share Skills to extend Claude's capabilities in Claude Code.
|
||||
|
||||
## What Are Agent Skills?
|
||||
|
||||
Agent Skills are modular capabilities that extend Claude's functionality. Each Skill packages:
|
||||
- Instructions and procedural knowledge
|
||||
- Metadata (name, description)
|
||||
- Optional resources (scripts, templates, references)
|
||||
|
||||
Skills are automatically discovered and used by Claude when relevant to the task.
|
||||
|
||||
## Skill Structure
|
||||
|
||||
### Basic Structure
|
||||
```
|
||||
.claude/skills/
|
||||
└── my-skill/
|
||||
├── skill.md # Instructions (required)
|
||||
└── skill.json # Metadata (required)
|
||||
```
|
||||
|
||||
### With Resources
|
||||
```
|
||||
.claude/skills/
|
||||
└── my-skill/
|
||||
├── skill.md
|
||||
├── skill.json
|
||||
├── scripts/ # Executable code
|
||||
├── references/ # Documentation
|
||||
└── assets/ # Templates, images
|
||||
```
|
||||
|
||||
## Creating Skills
|
||||
|
||||
### skill.json
|
||||
Metadata and configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-skill",
|
||||
"description": "Brief description of when to use this skill",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name"
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
- `name`: Unique identifier (kebab-case)
|
||||
- `description`: When Claude should activate this skill
|
||||
- `version`: Semantic version
|
||||
- `author`: Creator name or org
|
||||
|
||||
### skill.md
|
||||
Main instructions for Claude:
|
||||
|
||||
```markdown
|
||||
# Skill Name
|
||||
|
||||
Description of what this skill does.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Specific scenarios when Claude should activate this skill.
|
||||
|
||||
## Instructions
|
||||
|
||||
Step-by-step instructions for Claude to follow.
|
||||
|
||||
## Examples
|
||||
|
||||
Concrete examples of skill usage.
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Clear Activation Criteria
|
||||
Define exactly when the skill should be used:
|
||||
|
||||
**Good:**
|
||||
```
|
||||
Use when creating React components with TypeScript and Tailwind CSS.
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
Use for frontend development.
|
||||
```
|
||||
|
||||
### Concise Instructions
|
||||
Focus on essential information, avoid duplication:
|
||||
|
||||
**Good:**
|
||||
```
|
||||
1. Create component file in src/components/
|
||||
2. Use TypeScript interfaces for props
|
||||
3. Apply Tailwind classes for styling
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
First you need to think about creating a component,
|
||||
then maybe you should consider...
|
||||
```
|
||||
|
||||
### Actionable Guidance
|
||||
Provide clear steps Claude can follow:
|
||||
|
||||
**Good:**
|
||||
```
|
||||
Run `npm test` to validate implementation.
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
You might want to test things.
|
||||
```
|
||||
|
||||
### Include Examples
|
||||
Show concrete input/output examples:
|
||||
|
||||
```markdown
|
||||
## Examples
|
||||
|
||||
Input: "Create button component"
|
||||
Output: Creates src/components/Button.tsx with props interface
|
||||
```
|
||||
|
||||
### Scope Limitation
|
||||
Keep skills focused on specific domains:
|
||||
|
||||
**Good:**
|
||||
- `api-testing` - Testing REST APIs
|
||||
- `db-migrations` - Database schema changes
|
||||
|
||||
**Bad:**
|
||||
- `general-development` - Everything
|
||||
|
||||
## Resource Types
|
||||
|
||||
### Scripts (`scripts/`)
|
||||
Executable code for deterministic tasks:
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── format-code.py
|
||||
├── generate-types.js
|
||||
└── run-tests.sh
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Repeated code generation
|
||||
- Deterministic transformations
|
||||
- External tool integrations
|
||||
|
||||
### References (`references/`)
|
||||
Documentation loaded into context as needed:
|
||||
|
||||
```
|
||||
references/
|
||||
├── api-docs.md
|
||||
├── schemas.md
|
||||
└── workflows.md
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- API documentation
|
||||
- Database schemas
|
||||
- Domain knowledge
|
||||
- Detailed workflows
|
||||
|
||||
### Assets (`assets/`)
|
||||
Files used in output:
|
||||
|
||||
```
|
||||
assets/
|
||||
├── templates/
|
||||
│ └── component-template.tsx
|
||||
├── icons/
|
||||
└── configs/
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Templates
|
||||
- Boilerplate code
|
||||
- Images, icons
|
||||
- Configuration files
|
||||
|
||||
## Using Skills via API
|
||||
|
||||
### TypeScript Example
|
||||
```typescript
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
const client = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY
|
||||
});
|
||||
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
max_tokens: 4096,
|
||||
skills: [
|
||||
{
|
||||
type: 'custom',
|
||||
custom: {
|
||||
name: 'document-creator',
|
||||
description: 'Creates professional documents',
|
||||
instructions: 'Follow corporate style guide...'
|
||||
}
|
||||
}
|
||||
],
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: 'Create a project proposal'
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### Python Example
|
||||
```python
|
||||
from anthropic import Anthropic
|
||||
|
||||
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
|
||||
response = client.messages.create(
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
max_tokens=4096,
|
||||
skills=[
|
||||
{
|
||||
"type": "custom",
|
||||
"custom": {
|
||||
"name": "code-reviewer",
|
||||
"description": "Reviews code for quality and security",
|
||||
"instructions": "Check for common issues..."
|
||||
}
|
||||
}
|
||||
],
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": "Review this code"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
## Skill Discovery
|
||||
|
||||
Claude automatically discovers skills:
|
||||
|
||||
1. **Global skills**: `~/.claude/skills/`
|
||||
2. **Project skills**: `.claude/skills/`
|
||||
3. **Plugin skills**: From installed plugins
|
||||
|
||||
Skills are activated when:
|
||||
- Task matches skill description
|
||||
- User explicitly invokes skill
|
||||
- Context suggests skill is relevant
|
||||
|
||||
## Managing Skills
|
||||
|
||||
### List Skills
|
||||
```bash
|
||||
claude skills list
|
||||
```
|
||||
|
||||
### Test Skill
|
||||
```bash
|
||||
claude --skill my-skill "test task"
|
||||
```
|
||||
|
||||
### Share Skill
|
||||
```bash
|
||||
# Package skill
|
||||
cd .claude/skills/my-skill
|
||||
tar -czf my-skill.tar.gz .
|
||||
|
||||
# Or create plugin
|
||||
# See references/hooks-and-plugins.md
|
||||
```
|
||||
|
||||
### Install Skill
|
||||
```bash
|
||||
# Manual installation
|
||||
cd .claude/skills/
|
||||
tar -xzf my-skill.tar.gz
|
||||
```
|
||||
|
||||
## Example Skills
|
||||
|
||||
### API Testing Skill
|
||||
|
||||
**skill.json:**
|
||||
```json
|
||||
{
|
||||
"name": "api-testing",
|
||||
"description": "Test REST APIs with automated requests",
|
||||
"version": "1.0.0",
|
||||
"author": "Team"
|
||||
}
|
||||
```
|
||||
|
||||
**skill.md:**
|
||||
```markdown
|
||||
# API Testing
|
||||
|
||||
Test REST APIs with comprehensive validation.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use when testing API endpoints, validating responses, or
|
||||
creating API test suites.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Read API documentation from references/api-docs.md
|
||||
2. Use scripts/test-api.py for making requests
|
||||
3. Validate response status, headers, body
|
||||
4. Generate test report
|
||||
|
||||
## Examples
|
||||
|
||||
Request: "Test the /users endpoint"
|
||||
Actions:
|
||||
- Read references/api-docs.md for endpoint spec
|
||||
- Run scripts/test-api.py --endpoint /users
|
||||
- Validate response matches schema
|
||||
- Report results
|
||||
```
|
||||
|
||||
### Database Migration Skill
|
||||
|
||||
**skill.json:**
|
||||
```json
|
||||
{
|
||||
"name": "db-migrations",
|
||||
"description": "Create and manage database migrations",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
**skill.md:**
|
||||
```markdown
|
||||
# Database Migrations
|
||||
|
||||
Create safe, reversible database schema changes.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use when modifying database schema, adding tables,
|
||||
or changing column definitions.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Review current schema in references/schema.md
|
||||
2. Create migration file in migrations/
|
||||
3. Include both up and down migrations
|
||||
4. Test migration on development database
|
||||
5. Update references/schema.md
|
||||
|
||||
## Migration Template
|
||||
|
||||
See assets/migration-template.sql for standard format.
|
||||
```
|
||||
|
||||
## Progressive Disclosure
|
||||
|
||||
Keep skill.md concise (<200 lines) by:
|
||||
|
||||
1. **Core instructions** in skill.md
|
||||
2. **Detailed docs** in references/
|
||||
3. **Executable code** in scripts/
|
||||
4. **Templates** in assets/
|
||||
|
||||
Example structure:
|
||||
```markdown
|
||||
# My Skill
|
||||
|
||||
Brief overview.
|
||||
|
||||
## When to Use
|
||||
|
||||
Clear activation criteria.
|
||||
|
||||
## Instructions
|
||||
|
||||
High-level steps that reference:
|
||||
- references/detailed-workflow.md
|
||||
- scripts/automation.py
|
||||
- assets/template.tsx
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Skill Not Activating
|
||||
- Check description specificity
|
||||
- Verify skill.json format
|
||||
- Ensure skill.md has clear activation criteria
|
||||
|
||||
### Resource Not Found
|
||||
- Verify file paths in skill.md
|
||||
- Check directory structure
|
||||
- Use relative paths from skill root
|
||||
|
||||
### Conflicting Skills
|
||||
- Make descriptions more specific
|
||||
- Use unique names
|
||||
- Scope skills narrowly
|
||||
|
||||
## See Also
|
||||
|
||||
- Skill creation guide: https://docs.claude.com/claude-code/skills
|
||||
- Best practices: https://docs.claude.com/agents-and-tools/agent-skills/best-practices
|
||||
- API usage: `references/api-reference.md`
|
||||
- Plugin system: `references/hooks-and-plugins.md`
|
||||
498
skills/claude-code/references/api-reference.md
Normal file
498
skills/claude-code/references/api-reference.md
Normal file
@@ -0,0 +1,498 @@
|
||||
# API Reference
|
||||
|
||||
API endpoints and programmatic access to Claude Code functionality.
|
||||
|
||||
## Admin API
|
||||
|
||||
### Usage Reports
|
||||
|
||||
**Get Claude Code Usage Report:**
|
||||
```bash
|
||||
GET /v1/admin/claude-code/usage
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `start_date`: Start date (YYYY-MM-DD)
|
||||
- `end_date`: End date (YYYY-MM-DD)
|
||||
- `user_id`: Filter by user
|
||||
- `workspace_id`: Filter by workspace
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"usage": [
|
||||
{
|
||||
"date": "2025-11-06",
|
||||
"user_id": "user-123",
|
||||
"requests": 150,
|
||||
"tokens": 45000,
|
||||
"cost": 12.50
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl https://api.anthropic.com/v1/admin/claude-code/usage \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d start_date=2025-11-01 \
|
||||
-d end_date=2025-11-06
|
||||
```
|
||||
|
||||
### Cost Reports
|
||||
|
||||
**Get Cost Report:**
|
||||
```bash
|
||||
GET /v1/admin/usage/cost
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `start_date`: Start date
|
||||
- `end_date`: End date
|
||||
- `group_by`: `user` | `project` | `model`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"costs": [
|
||||
{
|
||||
"group": "user-123",
|
||||
"input_tokens": 100000,
|
||||
"output_tokens": 50000,
|
||||
"cost": 25.00
|
||||
}
|
||||
],
|
||||
"total": 250.00
|
||||
}
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
**List Users:**
|
||||
```bash
|
||||
GET /v1/admin/users
|
||||
```
|
||||
|
||||
**Get User:**
|
||||
```bash
|
||||
GET /v1/admin/users/{user_id}
|
||||
```
|
||||
|
||||
**Update User:**
|
||||
```bash
|
||||
PATCH /v1/admin/users/{user_id}
|
||||
```
|
||||
|
||||
**Remove User:**
|
||||
```bash
|
||||
DELETE /v1/admin/users/{user_id}
|
||||
```
|
||||
|
||||
## Messages API
|
||||
|
||||
### Create Message
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
POST /v1/messages
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 4096,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Explain this code"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**With Skills:**
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 4096,
|
||||
"skills": [
|
||||
{
|
||||
"type": "custom",
|
||||
"custom": {
|
||||
"name": "code-reviewer",
|
||||
"description": "Reviews code quality",
|
||||
"instructions": "Check for bugs, security issues..."
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "msg_123",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "This code implements..."
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stream Messages
|
||||
|
||||
**Streaming response:**
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 4096,
|
||||
"stream": true,
|
||||
"messages": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**Server-sent events:**
|
||||
```
|
||||
event: message_start
|
||||
data: {"type":"message_start","message":{...}}
|
||||
|
||||
event: content_block_delta
|
||||
data: {"type":"content_block_delta","delta":{"text":"Hello"}}
|
||||
|
||||
event: message_stop
|
||||
data: {"type":"message_stop"}
|
||||
```
|
||||
|
||||
### Count Tokens
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
POST /v1/messages/count_tokens
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Count these tokens"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"input_tokens": 15
|
||||
}
|
||||
```
|
||||
|
||||
## Files API
|
||||
|
||||
### Upload File
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
POST /v1/files
|
||||
```
|
||||
|
||||
**Request (multipart/form-data):**
|
||||
```bash
|
||||
curl https://api.anthropic.com/v1/files \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-F file=@document.pdf \
|
||||
-F purpose=user_upload
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "file-123",
|
||||
"object": "file",
|
||||
"bytes": 12345,
|
||||
"created_at": 1699564800,
|
||||
"filename": "document.pdf",
|
||||
"purpose": "user_upload"
|
||||
}
|
||||
```
|
||||
|
||||
### List Files
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
GET /v1/files
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "file-123",
|
||||
"filename": "document.pdf",
|
||||
"bytes": 12345
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Download File
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
GET /v1/files/{file_id}/content
|
||||
```
|
||||
|
||||
### Delete File
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
DELETE /v1/files/{file_id}
|
||||
```
|
||||
|
||||
## Models API
|
||||
|
||||
### List Models
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
GET /v1/models
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-sonnet-4-5-20250929",
|
||||
"type": "model",
|
||||
"display_name": "Claude Sonnet 4.5"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get Model
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
GET /v1/models/{model_id}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "claude-sonnet-4-5-20250929",
|
||||
"type": "model",
|
||||
"display_name": "Claude Sonnet 4.5",
|
||||
"created_at": 1699564800
|
||||
}
|
||||
```
|
||||
|
||||
## Skills API
|
||||
|
||||
### Create Skill
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
POST /v1/skills
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "my-skill",
|
||||
"description": "Skill description",
|
||||
"instructions": "Detailed instructions...",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### List Skills
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
GET /v1/skills
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "skill-123",
|
||||
"name": "my-skill",
|
||||
"description": "Skill description"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Update Skill
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
PATCH /v1/skills/{skill_id}
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"description": "Updated description",
|
||||
"instructions": "Updated instructions..."
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Skill
|
||||
|
||||
**Endpoint:**
|
||||
```bash
|
||||
DELETE /v1/skills/{skill_id}
|
||||
```
|
||||
|
||||
## Client SDKs
|
||||
|
||||
### TypeScript/JavaScript
|
||||
|
||||
```typescript
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
const client = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY
|
||||
});
|
||||
|
||||
const message = await client.messages.create({
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello, Claude!' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(message.content);
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic(
|
||||
api_key=os.environ.get("ANTHROPIC_API_KEY")
|
||||
)
|
||||
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
max_tokens=1024,
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello, Claude!"}
|
||||
]
|
||||
)
|
||||
|
||||
print(message.content)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"message": "Invalid API key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
**invalid_request_error**: Invalid request parameters
|
||||
**authentication_error**: Invalid API key
|
||||
**permission_error**: Insufficient permissions
|
||||
**not_found_error**: Resource not found
|
||||
**rate_limit_error**: Rate limit exceeded
|
||||
**api_error**: Internal API error
|
||||
**overloaded_error**: Server overloaded
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```typescript
|
||||
async function withRetry(fn: () => Promise<any>, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 529 && i < maxRetries - 1) {
|
||||
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limits
|
||||
|
||||
### Headers
|
||||
|
||||
Response headers include rate limit info:
|
||||
```
|
||||
anthropic-ratelimit-requests-limit: 1000
|
||||
anthropic-ratelimit-requests-remaining: 999
|
||||
anthropic-ratelimit-requests-reset: 2025-11-06T12:00:00Z
|
||||
anthropic-ratelimit-tokens-limit: 100000
|
||||
anthropic-ratelimit-tokens-remaining: 99500
|
||||
anthropic-ratelimit-tokens-reset: 2025-11-06T12:00:00Z
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Monitor rate limit headers
|
||||
- Implement exponential backoff
|
||||
- Batch requests when possible
|
||||
- Use caching to reduce requests
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Key
|
||||
|
||||
Include API key in requests:
|
||||
```bash
|
||||
curl https://api.anthropic.com/v1/messages \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01"
|
||||
```
|
||||
|
||||
### Workspace Keys
|
||||
|
||||
For organization workspaces:
|
||||
```bash
|
||||
curl https://api.anthropic.com/v1/messages \
|
||||
-H "x-api-key: $WORKSPACE_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01"
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- API documentation: https://docs.anthropic.com/api
|
||||
- Client SDKs: https://docs.anthropic.com/api/client-sdks
|
||||
- Rate limits: https://docs.anthropic.com/api/rate-limits
|
||||
- Error handling: https://docs.anthropic.com/api/errors
|
||||
447
skills/claude-code/references/best-practices.md
Normal file
447
skills/claude-code/references/best-practices.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Best Practices
|
||||
|
||||
Guidelines for project organization, security, performance, collaboration, and cost management.
|
||||
|
||||
## Project Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Keep `.claude/` directory in version control:
|
||||
|
||||
```
|
||||
project/
|
||||
├── .claude/
|
||||
│ ├── settings.json # Project settings
|
||||
│ ├── commands/ # Custom slash commands
|
||||
│ │ ├── test-all.md
|
||||
│ │ └── deploy.md
|
||||
│ ├── skills/ # Project-specific skills
|
||||
│ │ └── api-testing/
|
||||
│ ├── hooks.json # Hooks configuration
|
||||
│ ├── mcp.json # MCP servers (no secrets!)
|
||||
│ └── .env.example # Environment template
|
||||
├── .gitignore # Ignore .claude/.env
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
Document custom extensions:
|
||||
|
||||
**README.md:**
|
||||
```markdown
|
||||
## Claude Code Setup
|
||||
|
||||
### Custom Commands
|
||||
- `/test-all`: Run full test suite
|
||||
- `/deploy`: Deploy to staging
|
||||
|
||||
### Skills
|
||||
- `api-testing`: REST API testing utilities
|
||||
|
||||
### MCP Servers
|
||||
- `postgres`: Database access
|
||||
- `github`: Repository integration
|
||||
|
||||
### Environment Variables
|
||||
Copy `.claude/.env.example` to `.claude/.env` and fill in:
|
||||
- GITHUB_TOKEN
|
||||
- DATABASE_URL
|
||||
```
|
||||
|
||||
### Team Sharing
|
||||
|
||||
**What to commit:**
|
||||
- `.claude/settings.json`
|
||||
- `.claude/commands/`
|
||||
- `.claude/skills/`
|
||||
- `.claude/hooks.json`
|
||||
- `.claude/mcp.json` (without secrets)
|
||||
- `.claude/.env.example`
|
||||
|
||||
**What NOT to commit:**
|
||||
- `.claude/.env` (contains secrets)
|
||||
- `.claude/memory/` (optional)
|
||||
- `.claude/cache/`
|
||||
- API keys or tokens
|
||||
|
||||
**.gitignore:**
|
||||
```
|
||||
.claude/.env
|
||||
.claude/memory/
|
||||
.claude/cache/
|
||||
.claude/logs/
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### API Key Management
|
||||
|
||||
**Never commit API keys:**
|
||||
```bash
|
||||
# Use environment variables
|
||||
export ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||
|
||||
# Or .env file (gitignored)
|
||||
echo 'ANTHROPIC_API_KEY=sk-ant-xxxxx' > .claude/.env
|
||||
```
|
||||
|
||||
**Rotate keys regularly:**
|
||||
```bash
|
||||
# Generate new key
|
||||
# Update in all environments
|
||||
# Revoke old key
|
||||
```
|
||||
|
||||
**Use workspace keys:**
|
||||
```bash
|
||||
# For team projects, use workspace API keys
|
||||
# Easier to manage and rotate
|
||||
# Better access control
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
Enable sandboxing in production:
|
||||
|
||||
```json
|
||||
{
|
||||
"sandboxing": {
|
||||
"enabled": true,
|
||||
"allowedPaths": ["/workspace"],
|
||||
"networkAccess": "restricted",
|
||||
"allowedDomains": ["api.company.com"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hook Security
|
||||
|
||||
Review hook scripts before execution:
|
||||
|
||||
```bash
|
||||
# Check hooks.json
|
||||
cat .claude/hooks.json | jq .
|
||||
|
||||
# Review scripts
|
||||
cat .claude/scripts/hook.sh
|
||||
|
||||
# Validate inputs in hooks
|
||||
#!/bin/bash
|
||||
if [[ ! "$TOOL_ARGS" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
||||
echo "Invalid input"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Plugin Security
|
||||
|
||||
Audit plugins before installation:
|
||||
|
||||
```bash
|
||||
# Review plugin source
|
||||
gh repo view username/plugin
|
||||
|
||||
# Check plugin.json
|
||||
tar -xzf plugin.tar.gz
|
||||
cat plugin.json
|
||||
|
||||
# Install from trusted sources only
|
||||
claude plugin install gh:anthropics/official-plugin
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose appropriate model for task:
|
||||
|
||||
**Haiku** - Fast, cost-effective:
|
||||
```bash
|
||||
claude --model haiku "fix typo in README"
|
||||
claude --model haiku "format code"
|
||||
```
|
||||
|
||||
**Sonnet** - Balanced (default):
|
||||
```bash
|
||||
claude "implement user authentication"
|
||||
claude "review this PR"
|
||||
```
|
||||
|
||||
**Opus** - Complex tasks:
|
||||
```bash
|
||||
claude --model opus "architect microservices system"
|
||||
claude --model opus "optimize algorithm performance"
|
||||
```
|
||||
|
||||
### Prompt Caching
|
||||
|
||||
Cache repeated context:
|
||||
|
||||
```typescript
|
||||
// Cache large codebase
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
system: [
|
||||
{
|
||||
type: 'text',
|
||||
text: largeCodebase,
|
||||
cache_control: { type: 'ephemeral' }
|
||||
}
|
||||
],
|
||||
messages: [...]
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 90% cost reduction on cached tokens
|
||||
- Faster responses
|
||||
- Better for iterative development
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Implement rate limiting in hooks:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .claude/scripts/rate-limit.sh
|
||||
|
||||
REQUESTS_FILE=".claude/requests.log"
|
||||
MAX_REQUESTS=100
|
||||
WINDOW=3600 # 1 hour
|
||||
|
||||
# Count recent requests
|
||||
RECENT=$(find $REQUESTS_FILE -mmin -60 | wc -l)
|
||||
|
||||
if [ $RECENT -ge $MAX_REQUESTS ]; then
|
||||
echo "Rate limit exceeded"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo $(date) >> $REQUESTS_FILE
|
||||
```
|
||||
|
||||
### Token Management
|
||||
|
||||
Monitor token usage:
|
||||
|
||||
```bash
|
||||
# Check usage
|
||||
claude usage show
|
||||
|
||||
# Set limits
|
||||
claude config set maxTokens 8192
|
||||
|
||||
# Track costs
|
||||
claude analytics cost --group-by project
|
||||
```
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
### Standardize Commands
|
||||
|
||||
Create consistent slash commands:
|
||||
|
||||
```markdown
|
||||
# .claude/commands/test.md
|
||||
Run test suite with coverage report.
|
||||
|
||||
Options:
|
||||
- {{suite}}: Specific test suite (optional)
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/test
|
||||
/test unit
|
||||
/test integration
|
||||
```
|
||||
|
||||
### Share Skills
|
||||
|
||||
Create team skills via plugins:
|
||||
|
||||
```bash
|
||||
# Create team plugin
|
||||
cd .claude/plugins/team-plugin
|
||||
cat > plugin.json <<EOF
|
||||
{
|
||||
"name": "team-plugin",
|
||||
"skills": ["skills/*/"],
|
||||
"commands": ["commands/*.md"]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Package and share
|
||||
tar -czf team-plugin.tar.gz .
|
||||
```
|
||||
|
||||
### Consistent Settings
|
||||
|
||||
Use project settings for consistency:
|
||||
|
||||
**.claude/settings.json:**
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"maxTokens": 8192,
|
||||
"outputStyle": "technical-writer",
|
||||
"thinking": {
|
||||
"enabled": true,
|
||||
"budget": 10000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Settings
|
||||
|
||||
Use project memory for shared context:
|
||||
|
||||
```json
|
||||
{
|
||||
"memory": {
|
||||
"enabled": true,
|
||||
"location": "project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Shared project knowledge
|
||||
- Consistent behavior across team
|
||||
- Reduced onboarding time
|
||||
|
||||
## Cost Management
|
||||
|
||||
### Budget Limits
|
||||
|
||||
Set budget limits in hooks:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .claude/scripts/budget-check.sh
|
||||
|
||||
MONTHLY_BUDGET=1000
|
||||
CURRENT_SPEND=$(claude analytics cost --format json | jq '.total')
|
||||
|
||||
if (( $(echo "$CURRENT_SPEND > $MONTHLY_BUDGET" | bc -l) )); then
|
||||
echo "⚠️ Monthly budget exceeded: \$$CURRENT_SPEND / \$$MONTHLY_BUDGET"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Usage Monitoring
|
||||
|
||||
Monitor via analytics API:
|
||||
|
||||
```bash
|
||||
# Daily usage report
|
||||
claude analytics usage --start $(date -d '1 day ago' +%Y-%m-%d)
|
||||
|
||||
# Cost by user
|
||||
claude analytics cost --group-by user
|
||||
|
||||
# Export to CSV
|
||||
claude analytics export --format csv > usage.csv
|
||||
```
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
**Use Haiku for simple tasks:**
|
||||
```bash
|
||||
# Expensive (Sonnet)
|
||||
claude "fix typo in README"
|
||||
|
||||
# Cheap (Haiku)
|
||||
claude --model haiku "fix typo in README"
|
||||
```
|
||||
|
||||
**Enable caching:**
|
||||
```json
|
||||
{
|
||||
"caching": {
|
||||
"enabled": true,
|
||||
"ttl": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Batch operations:**
|
||||
```bash
|
||||
# Instead of multiple requests
|
||||
claude "fix file1.js"
|
||||
claude "fix file2.js"
|
||||
claude "fix file3.js"
|
||||
|
||||
# Batch them
|
||||
claude "fix all files: file1.js file2.js file3.js"
|
||||
```
|
||||
|
||||
**Track per-project costs:**
|
||||
```bash
|
||||
# Tag projects
|
||||
claude --project my-project "implement feature"
|
||||
|
||||
# View project costs
|
||||
claude analytics cost --project my-project
|
||||
```
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Feature Development
|
||||
|
||||
```bash
|
||||
# 1. Plan feature
|
||||
claude /plan "implement user authentication"
|
||||
|
||||
# 2. Create checkpoint
|
||||
claude checkpoint create "before auth implementation"
|
||||
|
||||
# 3. Implement
|
||||
claude /cook "implement user authentication"
|
||||
|
||||
# 4. Test
|
||||
claude /test
|
||||
|
||||
# 5. Review
|
||||
claude "review authentication implementation"
|
||||
|
||||
# 6. Commit
|
||||
claude /git:cm
|
||||
```
|
||||
|
||||
### Bug Fixing
|
||||
|
||||
```bash
|
||||
# 1. Debug
|
||||
claude /debug "login button not working"
|
||||
|
||||
# 2. Fix
|
||||
claude /fix:fast "fix login button issue"
|
||||
|
||||
# 3. Test
|
||||
claude /test
|
||||
|
||||
# 4. Commit
|
||||
claude /git:cm
|
||||
```
|
||||
|
||||
### Code Review
|
||||
|
||||
```bash
|
||||
# Review PR
|
||||
claude "review PR #123"
|
||||
|
||||
# Check security
|
||||
claude "review for security vulnerabilities"
|
||||
|
||||
# Verify tests
|
||||
claude "check test coverage"
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Security guide: https://docs.claude.com/claude-code/security
|
||||
- Cost tracking: https://docs.claude.com/claude-code/costs
|
||||
- Team setup: https://docs.claude.com/claude-code/overview
|
||||
- API usage: `references/api-reference.md`
|
||||
428
skills/claude-code/references/cicd-integration.md
Normal file
428
skills/claude-code/references/cicd-integration.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# CI/CD Integration
|
||||
|
||||
Integrate Claude Code into development workflows with GitHub Actions and GitLab CI/CD.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
**.github/workflows/claude.yml:**
|
||||
```yaml
|
||||
name: Claude Code CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: anthropic/claude-code-action@v1
|
||||
with:
|
||||
command: '/fix:types && /test'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
### Code Review Workflow
|
||||
|
||||
```yaml
|
||||
name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Review with Claude
|
||||
uses: anthropic/claude-code-action@v1
|
||||
with:
|
||||
command: |
|
||||
Review the changes in this PR:
|
||||
- Check for bugs and edge cases
|
||||
- Verify test coverage
|
||||
- Assess performance implications
|
||||
- Review security concerns
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Post Review Comment
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: process.env.CLAUDE_OUTPUT
|
||||
})
|
||||
```
|
||||
|
||||
### Test & Fix Workflow
|
||||
|
||||
```yaml
|
||||
name: Test and Fix
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test-fix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Run Tests
|
||||
id: test
|
||||
continue-on-error: true
|
||||
run: npm test
|
||||
|
||||
- name: Fix Failures
|
||||
if: steps.test.outcome == 'failure'
|
||||
uses: anthropic/claude-code-action@v1
|
||||
with:
|
||||
command: '/fix:test check test output and fix failures'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Commit Fixes
|
||||
if: steps.test.outcome == 'failure'
|
||||
run: |
|
||||
git config user.name "Claude Bot"
|
||||
git config user.email "claude@anthropic.com"
|
||||
git add .
|
||||
git commit -m "fix: auto-fix test failures"
|
||||
git push
|
||||
```
|
||||
|
||||
### Documentation Update
|
||||
|
||||
```yaml
|
||||
name: Update Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Update Documentation
|
||||
uses: anthropic/claude-code-action@v1
|
||||
with:
|
||||
command: '/docs:update'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Commit Docs
|
||||
run: |
|
||||
git config user.name "Claude Bot"
|
||||
git config user.email "claude@anthropic.com"
|
||||
git add docs/
|
||||
git commit -m "docs: auto-update documentation" || echo "No changes"
|
||||
git push
|
||||
```
|
||||
|
||||
## GitLab CI/CD
|
||||
|
||||
### Basic Pipeline
|
||||
|
||||
**.gitlab-ci.yml:**
|
||||
```yaml
|
||||
stages:
|
||||
- review
|
||||
- test
|
||||
- deploy
|
||||
|
||||
claude-review:
|
||||
stage: review
|
||||
image: node:18
|
||||
script:
|
||||
- npm install -g @anthropic-ai/claude-code
|
||||
- claude login --api-key $ANTHROPIC_API_KEY
|
||||
- claude '/fix:types && /test'
|
||||
only:
|
||||
- merge_requests
|
||||
```
|
||||
|
||||
### Advanced Pipeline
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
CLAUDE_MODEL: "claude-sonnet-4-5-20250929"
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- review
|
||||
- deploy
|
||||
|
||||
before_script:
|
||||
- npm install -g @anthropic-ai/claude-code
|
||||
- claude login --api-key $ANTHROPIC_API_KEY
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
script:
|
||||
- claude '/fix:types'
|
||||
artifacts:
|
||||
paths:
|
||||
- src/
|
||||
expire_in: 1 hour
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- npm test || claude '/fix:test analyze failures and fix'
|
||||
coverage: '/Coverage: \d+\.\d+%/'
|
||||
|
||||
review:
|
||||
stage: review
|
||||
script:
|
||||
- |
|
||||
claude "Review this merge request:
|
||||
- Check code quality
|
||||
- Verify tests
|
||||
- Review security
|
||||
- Assess performance" > review.md
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: review.md
|
||||
only:
|
||||
- merge_requests
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
script:
|
||||
- claude '/deploy-check'
|
||||
- ./deploy.sh
|
||||
only:
|
||||
- main
|
||||
```
|
||||
|
||||
### Automated Fixes
|
||||
|
||||
```yaml
|
||||
fix-on-failure:
|
||||
stage: test
|
||||
script:
|
||||
- npm test
|
||||
retry:
|
||||
max: 2
|
||||
when:
|
||||
- script_failure
|
||||
after_script:
|
||||
- |
|
||||
if [ $CI_JOB_STATUS == 'failed' ]; then
|
||||
claude '/fix:test analyze CI logs and fix issues'
|
||||
git add .
|
||||
git commit -m "fix: auto-fix from CI"
|
||||
git push origin HEAD:$CI_COMMIT_REF_NAME
|
||||
fi
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### PR Comment Bot
|
||||
|
||||
Post Claude reviews as PR comments:
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Comment PR
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const review = process.env.CLAUDE_REVIEW
|
||||
github.rest.pulls.createReview({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
body: review,
|
||||
event: 'COMMENT'
|
||||
})
|
||||
```
|
||||
|
||||
### Conditional Execution
|
||||
|
||||
Run Claude only on certain conditions:
|
||||
|
||||
```yaml
|
||||
# Run on large PRs only
|
||||
- name: Review Large PRs
|
||||
if: ${{ github.event.pull_request.changed_files > 10 }}
|
||||
uses: anthropic/claude-code-action@v1
|
||||
with:
|
||||
command: '/review:codebase analyze changes'
|
||||
```
|
||||
|
||||
### Cost Control
|
||||
|
||||
Limit CI usage to control costs:
|
||||
|
||||
```yaml
|
||||
# Skip for draft PRs
|
||||
- name: Claude Review
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
uses: anthropic/claude-code-action@v1
|
||||
|
||||
# Run only on specific branches
|
||||
- name: Claude Check
|
||||
if: startsWith(github.ref, 'refs/heads/release/')
|
||||
uses: anthropic/claude-code-action@v1
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### API Key Management
|
||||
|
||||
**GitHub:**
|
||||
```
|
||||
Settings → Secrets and variables → Actions
|
||||
Add: ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
**GitLab:**
|
||||
```
|
||||
Settings → CI/CD → Variables
|
||||
Add: ANTHROPIC_API_KEY (Protected, Masked)
|
||||
```
|
||||
|
||||
### Restricted Permissions
|
||||
|
||||
**GitHub Actions:**
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
```
|
||||
|
||||
**GitLab CI:**
|
||||
```yaml
|
||||
variables:
|
||||
GIT_STRATEGY: clone
|
||||
GIT_DEPTH: 1
|
||||
```
|
||||
|
||||
### Secrets Scanning
|
||||
|
||||
Prevent API key exposure:
|
||||
|
||||
```yaml
|
||||
- name: Scan for Secrets
|
||||
run: |
|
||||
if git diff | grep -i "ANTHROPIC_API_KEY"; then
|
||||
echo "API key detected in diff!"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
### Workflow Logs
|
||||
|
||||
**GitHub Actions:**
|
||||
```yaml
|
||||
- name: Debug Info
|
||||
run: |
|
||||
echo "Workflow: ${{ github.workflow }}"
|
||||
echo "Event: ${{ github.event_name }}"
|
||||
echo "Ref: ${{ github.ref }}"
|
||||
```
|
||||
|
||||
**GitLab CI:**
|
||||
```yaml
|
||||
debug:
|
||||
script:
|
||||
- echo "Pipeline ID: $CI_PIPELINE_ID"
|
||||
- echo "Job ID: $CI_JOB_ID"
|
||||
- echo "Branch: $CI_COMMIT_BRANCH"
|
||||
```
|
||||
|
||||
### Artifacts
|
||||
|
||||
Save Claude outputs:
|
||||
|
||||
```yaml
|
||||
# GitHub
|
||||
- name: Save Claude Output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: claude-results
|
||||
path: claude-output.md
|
||||
|
||||
# GitLab
|
||||
artifacts:
|
||||
paths:
|
||||
- claude-output.md
|
||||
expire_in: 1 week
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```yaml
|
||||
- name: Claude Task
|
||||
continue-on-error: true
|
||||
id: claude
|
||||
uses: anthropic/claude-code-action@v1
|
||||
|
||||
- name: Handle Failure
|
||||
if: steps.claude.outcome == 'failure'
|
||||
run: |
|
||||
echo "Claude task failed, continuing anyway"
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Caching
|
||||
|
||||
**GitHub Actions:**
|
||||
```yaml
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.claude/cache
|
||||
key: claude-cache-${{ hashFiles('package-lock.json') }}
|
||||
```
|
||||
|
||||
**GitLab CI:**
|
||||
```yaml
|
||||
cache:
|
||||
key: claude-cache
|
||||
paths:
|
||||
- .claude/cache
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```yaml
|
||||
# GitHub - Matrix builds
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, test, review]
|
||||
steps:
|
||||
- run: claude "/${{ matrix.task }}"
|
||||
|
||||
# GitLab - Parallel jobs
|
||||
test:
|
||||
parallel: 3
|
||||
script:
|
||||
- claude "/test --shard $CI_NODE_INDEX/$CI_NODE_TOTAL"
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- GitHub Actions docs: https://docs.github.com/actions
|
||||
- GitLab CI/CD docs: https://docs.gitlab.com/ee/ci/
|
||||
- Claude Code Actions: https://github.com/anthropics/claude-code-action
|
||||
- Best practices: `references/best-practices.md`
|
||||
480
skills/claude-code/references/configuration.md
Normal file
480
skills/claude-code/references/configuration.md
Normal file
@@ -0,0 +1,480 @@
|
||||
# Configuration and Settings
|
||||
|
||||
Configure Claude Code behavior with settings hierarchy, model selection, and output styles.
|
||||
|
||||
## Settings Hierarchy
|
||||
|
||||
Settings are applied in order of precedence:
|
||||
|
||||
1. **Command-line flags** (highest priority)
|
||||
2. **Environment variables**
|
||||
3. **Project settings** (`.claude/settings.json`)
|
||||
4. **Global settings** (`~/.claude/settings.json`)
|
||||
|
||||
## Settings File Format
|
||||
|
||||
### Global Settings
|
||||
`~/.claude/settings.json`:
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"maxTokens": 8192,
|
||||
"temperature": 1.0,
|
||||
"thinking": {
|
||||
"enabled": true,
|
||||
"budget": 10000
|
||||
},
|
||||
"outputStyle": "default",
|
||||
"memory": {
|
||||
"enabled": true,
|
||||
"location": "global"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project Settings
|
||||
`.claude/settings.json`:
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"maxTokens": 4096,
|
||||
"sandboxing": {
|
||||
"enabled": true,
|
||||
"allowedPaths": ["/workspace"]
|
||||
},
|
||||
"memory": {
|
||||
"enabled": true,
|
||||
"location": "project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Settings
|
||||
|
||||
### Model Configuration
|
||||
|
||||
**model**: Claude model to use
|
||||
- `claude-sonnet-4-5-20250929` (default, latest Sonnet)
|
||||
- `claude-opus-4-20250514` (Opus for complex tasks)
|
||||
- `claude-haiku-4-20250408` (Haiku for speed)
|
||||
|
||||
**Model aliases:**
|
||||
- `sonnet`: Latest Claude Sonnet
|
||||
- `opus`: Latest Claude Opus
|
||||
- `haiku`: Latest Claude Haiku
|
||||
- `opusplan`: Opus with extended thinking for planning
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "sonnet"
|
||||
}
|
||||
```
|
||||
|
||||
### Token Settings
|
||||
|
||||
**maxTokens**: Maximum tokens in response
|
||||
- Default: 8192
|
||||
- Range: 1-200000
|
||||
|
||||
```json
|
||||
{
|
||||
"maxTokens": 16384
|
||||
}
|
||||
```
|
||||
|
||||
**temperature**: Randomness in responses
|
||||
- Default: 1.0
|
||||
- Range: 0.0-1.0
|
||||
- Lower = more focused, higher = more creative
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 0.7
|
||||
}
|
||||
```
|
||||
|
||||
### Thinking Configuration
|
||||
|
||||
**Extended thinking** for complex reasoning:
|
||||
|
||||
```json
|
||||
{
|
||||
"thinking": {
|
||||
"enabled": true,
|
||||
"budget": 10000,
|
||||
"mode": "auto"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `enabled`: Enable extended thinking
|
||||
- `budget`: Token budget for thinking (default: 10000)
|
||||
- `mode`: `auto` | `manual` | `disabled`
|
||||
|
||||
### Sandboxing
|
||||
|
||||
Filesystem and network isolation:
|
||||
|
||||
```json
|
||||
{
|
||||
"sandboxing": {
|
||||
"enabled": true,
|
||||
"allowedPaths": [
|
||||
"/workspace",
|
||||
"/home/user/projects"
|
||||
],
|
||||
"networkAccess": "restricted",
|
||||
"allowedDomains": [
|
||||
"api.example.com",
|
||||
"*.trusted.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `enabled`: Enable sandboxing
|
||||
- `allowedPaths`: Filesystem access paths
|
||||
- `networkAccess`: `full` | `restricted` | `none`
|
||||
- `allowedDomains`: Whitelisted domains
|
||||
|
||||
### Memory Management
|
||||
|
||||
Control how Claude remembers context:
|
||||
|
||||
```json
|
||||
{
|
||||
"memory": {
|
||||
"enabled": true,
|
||||
"location": "project",
|
||||
"ttl": 86400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**location options:**
|
||||
- `global`: Share memory across all projects
|
||||
- `project`: Project-specific memory
|
||||
- `none`: Disable memory
|
||||
|
||||
**ttl**: Time to live in seconds (default: 86400 = 24 hours)
|
||||
|
||||
### Output Styles
|
||||
|
||||
Customize Claude's behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"outputStyle": "technical-writer"
|
||||
}
|
||||
```
|
||||
|
||||
**Built-in styles:**
|
||||
- `default`: Standard coding assistant
|
||||
- `technical-writer`: Documentation focus
|
||||
- `code-reviewer`: Review-focused
|
||||
- `minimal`: Concise responses
|
||||
|
||||
### Logging
|
||||
|
||||
Configure logging behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": ".claude/logs/session.log",
|
||||
"console": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Levels:** `debug`, `info`, `warn`, `error`
|
||||
|
||||
## Model Configuration
|
||||
|
||||
### Using Model Aliases
|
||||
|
||||
```bash
|
||||
# Use Sonnet (default)
|
||||
claude
|
||||
|
||||
# Use Opus for complex task
|
||||
claude --model opus "architect a microservices system"
|
||||
|
||||
# Use Haiku for speed
|
||||
claude --model haiku "fix typo in README"
|
||||
|
||||
# Use opusplan for planning
|
||||
claude --model opusplan "plan authentication system"
|
||||
```
|
||||
|
||||
### In Settings File
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "opus",
|
||||
"thinking": {
|
||||
"enabled": true,
|
||||
"budget": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Selection Guide
|
||||
|
||||
**Sonnet** (claude-sonnet-4-5-20250929):
|
||||
- Balanced performance and cost
|
||||
- Default choice for most tasks
|
||||
- Good for general development
|
||||
|
||||
**Opus** (claude-opus-4-20250514):
|
||||
- Highest capability
|
||||
- Complex reasoning and planning
|
||||
- Use for architecture, design, complex debugging
|
||||
|
||||
**Haiku** (claude-haiku-4-20250408):
|
||||
- Fastest, most cost-effective
|
||||
- Simple tasks (typos, formatting)
|
||||
- High-volume operations
|
||||
|
||||
**opusplan**:
|
||||
- Opus + extended thinking
|
||||
- Deep planning and analysis
|
||||
- Architecture decisions
|
||||
|
||||
## Output Styles
|
||||
|
||||
### Creating Custom Output Style
|
||||
|
||||
Create `~/.claude/output-styles/my-style.md`:
|
||||
|
||||
```markdown
|
||||
You are a senior software architect focused on scalability.
|
||||
|
||||
Guidelines:
|
||||
- Prioritize performance and scalability
|
||||
- Consider distributed systems patterns
|
||||
- Include monitoring and observability
|
||||
- Think about failure modes
|
||||
- Document trade-offs
|
||||
```
|
||||
|
||||
### Using Custom Output Style
|
||||
|
||||
```bash
|
||||
claude --output-style my-style
|
||||
```
|
||||
|
||||
Or in settings:
|
||||
```json
|
||||
{
|
||||
"outputStyle": "my-style"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Output Styles
|
||||
|
||||
**technical-writer.md:**
|
||||
```markdown
|
||||
You are a technical writer creating clear documentation.
|
||||
|
||||
Guidelines:
|
||||
- Use simple, clear language
|
||||
- Provide examples
|
||||
- Structure with headings
|
||||
- Include diagrams when helpful
|
||||
- Focus on user understanding
|
||||
```
|
||||
|
||||
**code-reviewer.md:**
|
||||
```markdown
|
||||
You are a senior code reviewer.
|
||||
|
||||
Guidelines:
|
||||
- Check for bugs and edge cases
|
||||
- Review security vulnerabilities
|
||||
- Assess performance implications
|
||||
- Verify test coverage
|
||||
- Suggest improvements
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### API Configuration
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||
export ANTHROPIC_BASE_URL=https://api.anthropic.com
|
||||
```
|
||||
|
||||
### Proxy Configuration
|
||||
```bash
|
||||
export HTTP_PROXY=http://proxy.company.com:8080
|
||||
export HTTPS_PROXY=http://proxy.company.com:8080
|
||||
export NO_PROXY=localhost,127.0.0.1
|
||||
```
|
||||
|
||||
### Custom CA Certificates
|
||||
```bash
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-bundle.crt
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
export CLAUDE_DEBUG=1
|
||||
export CLAUDE_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
## Command-Line Flags
|
||||
|
||||
### Common Flags
|
||||
|
||||
```bash
|
||||
# Set model
|
||||
claude --model opus
|
||||
|
||||
# Set max tokens
|
||||
claude --max-tokens 16384
|
||||
|
||||
# Set temperature
|
||||
claude --temperature 0.8
|
||||
|
||||
# Enable debug mode
|
||||
claude --debug
|
||||
|
||||
# Use specific output style
|
||||
claude --output-style technical-writer
|
||||
|
||||
# Disable memory
|
||||
claude --no-memory
|
||||
|
||||
# Set project directory
|
||||
claude --project /path/to/project
|
||||
```
|
||||
|
||||
### Configuration Commands
|
||||
|
||||
```bash
|
||||
# View current settings
|
||||
claude config list
|
||||
|
||||
# Set global setting
|
||||
claude config set model opus
|
||||
|
||||
# Set project setting
|
||||
claude config set --project maxTokens 4096
|
||||
|
||||
# Get specific setting
|
||||
claude config get model
|
||||
|
||||
# Reset to defaults
|
||||
claude config reset
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Tools
|
||||
|
||||
Register custom tools:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "custom-tool",
|
||||
"description": "Custom tool",
|
||||
"command": "./scripts/custom-tool.sh",
|
||||
"parameters": {
|
||||
"arg1": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Configure rate limits:
|
||||
|
||||
```json
|
||||
{
|
||||
"rateLimits": {
|
||||
"requestsPerMinute": 100,
|
||||
"tokensPerMinute": 100000,
|
||||
"retryStrategy": "exponential"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
Prompt caching configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"caching": {
|
||||
"enabled": true,
|
||||
"ttl": 3600,
|
||||
"maxSize": "100MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Project Settings
|
||||
- Keep project-specific in `.claude/settings.json`
|
||||
- Commit to version control
|
||||
- Document custom settings
|
||||
- Share with team
|
||||
|
||||
### Global Settings
|
||||
- Personal preferences only
|
||||
- Don't override project settings unnecessarily
|
||||
- Use for API keys and auth
|
||||
|
||||
### Security
|
||||
- Never commit API keys
|
||||
- Use environment variables for secrets
|
||||
- Enable sandboxing in production
|
||||
- Restrict network access
|
||||
|
||||
### Performance
|
||||
- Use appropriate model for task
|
||||
- Set reasonable token limits
|
||||
- Enable caching
|
||||
- Configure rate limits
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Settings Not Applied
|
||||
```bash
|
||||
# Check settings hierarchy
|
||||
claude config list --all
|
||||
|
||||
# Verify settings file syntax
|
||||
cat .claude/settings.json | jq .
|
||||
|
||||
# Reset to defaults
|
||||
claude config reset
|
||||
```
|
||||
|
||||
### Environment Variables Not Recognized
|
||||
```bash
|
||||
# Verify export
|
||||
echo $ANTHROPIC_API_KEY
|
||||
|
||||
# Check shell profile
|
||||
cat ~/.bashrc | grep ANTHROPIC
|
||||
|
||||
# Reload shell
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Model selection: https://docs.claude.com/about-claude/models
|
||||
- Output styles: `references/best-practices.md`
|
||||
- Security: `references/enterprise-features.md`
|
||||
- Troubleshooting: `references/troubleshooting.md`
|
||||
472
skills/claude-code/references/enterprise-features.md
Normal file
472
skills/claude-code/references/enterprise-features.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# Enterprise Features
|
||||
|
||||
Enterprise deployment, security, compliance, and monitoring for Claude Code.
|
||||
|
||||
## Identity & Access Management
|
||||
|
||||
### SSO Integration
|
||||
|
||||
Support for SAML 2.0 and OAuth 2.0:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"type": "saml",
|
||||
"provider": "okta",
|
||||
"entityId": "claude-code",
|
||||
"ssoUrl": "https://company.okta.com/app/saml",
|
||||
"certificate": "/path/to/cert.pem"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Supported providers:**
|
||||
- Okta
|
||||
- Azure AD
|
||||
- Google Workspace
|
||||
- OneLogin
|
||||
- Auth0
|
||||
|
||||
### Role-Based Access Control (RBAC)
|
||||
|
||||
Define user roles and permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"rbac": {
|
||||
"roles": {
|
||||
"developer": {
|
||||
"permissions": ["code:read", "code:write", "tools:use"]
|
||||
},
|
||||
"reviewer": {
|
||||
"permissions": ["code:read", "code:review"]
|
||||
},
|
||||
"admin": {
|
||||
"permissions": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
Centralized user provisioning:
|
||||
|
||||
```bash
|
||||
# Add user
|
||||
claude admin user add user@company.com --role developer
|
||||
|
||||
# Remove user
|
||||
claude admin user remove user@company.com
|
||||
|
||||
# List users
|
||||
claude admin user list
|
||||
|
||||
# Update user role
|
||||
claude admin user update user@company.com --role admin
|
||||
```
|
||||
|
||||
## Security & Compliance
|
||||
|
||||
### Sandboxing
|
||||
|
||||
Filesystem and network isolation:
|
||||
|
||||
```json
|
||||
{
|
||||
"sandboxing": {
|
||||
"enabled": true,
|
||||
"mode": "strict",
|
||||
"filesystem": {
|
||||
"allowedPaths": ["/workspace"],
|
||||
"readOnlyPaths": ["/usr/lib", "/etc"],
|
||||
"deniedPaths": ["/etc/passwd", "/etc/shadow"]
|
||||
},
|
||||
"network": {
|
||||
"enabled": false,
|
||||
"allowedDomains": ["api.anthropic.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Audit Logging
|
||||
|
||||
Comprehensive activity logs:
|
||||
|
||||
```json
|
||||
{
|
||||
"auditLog": {
|
||||
"enabled": true,
|
||||
"destination": "syslog",
|
||||
"syslogHost": "logs.company.com:514",
|
||||
"includeToolCalls": true,
|
||||
"includePrompts": false,
|
||||
"retention": "90d"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Log format:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-11-06T10:30:00Z",
|
||||
"user": "user@company.com",
|
||||
"action": "tool_call",
|
||||
"tool": "bash",
|
||||
"args": {"command": "git status"},
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### Data Residency
|
||||
|
||||
Region-specific deployment:
|
||||
|
||||
```json
|
||||
{
|
||||
"region": "us-east-1",
|
||||
"dataResidency": {
|
||||
"enabled": true,
|
||||
"allowedRegions": ["us-east-1", "us-west-2"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Compliance Certifications
|
||||
|
||||
- **SOC 2 Type II**: Security controls
|
||||
- **HIPAA**: Healthcare data protection
|
||||
- **GDPR**: EU data protection
|
||||
- **ISO 27001**: Information security
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
Deploy via AWS Bedrock:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "bedrock",
|
||||
"region": "us-east-1",
|
||||
"model": "anthropic.claude-sonnet-4-5",
|
||||
"credentials": {
|
||||
"accessKeyId": "${AWS_ACCESS_KEY_ID}",
|
||||
"secretAccessKey": "${AWS_SECRET_ACCESS_KEY}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Google Vertex AI
|
||||
|
||||
Deploy via GCP Vertex AI:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "vertex",
|
||||
"project": "company-project",
|
||||
"location": "us-central1",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"credentials": "/path/to/service-account.json"
|
||||
}
|
||||
```
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
On-premises deployment:
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker run -d \
|
||||
-v /workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=$API_KEY \
|
||||
anthropic/claude-code:latest
|
||||
```
|
||||
|
||||
**Kubernetes:**
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: claude-code
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: claude-code
|
||||
image: anthropic/claude-code:latest
|
||||
env:
|
||||
- name: ANTHROPIC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: claude-secrets
|
||||
key: api-key
|
||||
```
|
||||
|
||||
### LLM Gateway
|
||||
|
||||
Integration with LiteLLM:
|
||||
|
||||
```json
|
||||
{
|
||||
"gateway": {
|
||||
"enabled": true,
|
||||
"url": "http://litellm-proxy:4000",
|
||||
"apiKey": "${GATEWAY_API_KEY}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring & Analytics
|
||||
|
||||
### OpenTelemetry
|
||||
|
||||
Built-in telemetry support:
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"exporter": "otlp",
|
||||
"endpoint": "http://otel-collector:4317",
|
||||
"metrics": true,
|
||||
"traces": true,
|
||||
"logs": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Analytics
|
||||
|
||||
Track team productivity metrics:
|
||||
|
||||
```bash
|
||||
# Get usage report
|
||||
claude analytics usage --start 2025-11-01 --end 2025-11-06
|
||||
|
||||
# Get cost report
|
||||
claude analytics cost --group-by user
|
||||
|
||||
# Export metrics
|
||||
claude analytics export --format csv > metrics.csv
|
||||
```
|
||||
|
||||
**Metrics tracked:**
|
||||
- Requests per user/project
|
||||
- Token usage
|
||||
- Tool invocations
|
||||
- Session duration
|
||||
- Error rates
|
||||
- Cost per user/project
|
||||
|
||||
### Custom Dashboards
|
||||
|
||||
Build org-specific dashboards:
|
||||
|
||||
```python
|
||||
from claude_code import Analytics
|
||||
|
||||
analytics = Analytics(api_key=API_KEY)
|
||||
|
||||
# Get metrics
|
||||
metrics = analytics.get_metrics(
|
||||
start="2025-11-01",
|
||||
end="2025-11-06",
|
||||
group_by="user"
|
||||
)
|
||||
|
||||
# Create visualization
|
||||
dashboard = analytics.create_dashboard(
|
||||
metrics=metrics,
|
||||
charts=["usage", "cost", "errors"]
|
||||
)
|
||||
```
|
||||
|
||||
### Cost Management
|
||||
|
||||
Monitor and control API costs:
|
||||
|
||||
```json
|
||||
{
|
||||
"costControl": {
|
||||
"enabled": true,
|
||||
"budgets": {
|
||||
"monthly": 10000,
|
||||
"perUser": 500
|
||||
},
|
||||
"alerts": {
|
||||
"threshold": 0.8,
|
||||
"recipients": ["admin@company.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network Configuration
|
||||
|
||||
### Proxy Support
|
||||
|
||||
HTTP/HTTPS proxy configuration:
|
||||
|
||||
```bash
|
||||
export HTTP_PROXY=http://proxy.company.com:8080
|
||||
export HTTPS_PROXY=http://proxy.company.com:8080
|
||||
export NO_PROXY=localhost,127.0.0.1,company.internal
|
||||
```
|
||||
|
||||
### Custom CA
|
||||
|
||||
Trust custom certificate authorities:
|
||||
|
||||
```bash
|
||||
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/company-ca.crt
|
||||
```
|
||||
|
||||
### Mutual TLS (mTLS)
|
||||
|
||||
Client certificate authentication:
|
||||
|
||||
```json
|
||||
{
|
||||
"mtls": {
|
||||
"enabled": true,
|
||||
"clientCert": "/path/to/client-cert.pem",
|
||||
"clientKey": "/path/to/client-key.pem",
|
||||
"caCert": "/path/to/ca-cert.pem"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IP Allowlisting
|
||||
|
||||
Restrict access by IP:
|
||||
|
||||
```json
|
||||
{
|
||||
"ipAllowlist": {
|
||||
"enabled": true,
|
||||
"addresses": [
|
||||
"10.0.0.0/8",
|
||||
"192.168.1.0/24",
|
||||
"203.0.113.42"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Governance
|
||||
|
||||
### Data Retention
|
||||
|
||||
Configure data retention policies:
|
||||
|
||||
```json
|
||||
{
|
||||
"dataRetention": {
|
||||
"conversations": "30d",
|
||||
"logs": "90d",
|
||||
"metrics": "1y",
|
||||
"backups": "7d"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Encryption
|
||||
|
||||
Encryption at rest and in transit:
|
||||
|
||||
```json
|
||||
{
|
||||
"encryption": {
|
||||
"atRest": {
|
||||
"enabled": true,
|
||||
"algorithm": "AES-256-GCM",
|
||||
"keyManagement": "aws-kms"
|
||||
},
|
||||
"inTransit": {
|
||||
"tlsVersion": "1.3",
|
||||
"cipherSuites": ["TLS_AES_256_GCM_SHA384"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PII Protection
|
||||
|
||||
Detect and redact PII:
|
||||
|
||||
```json
|
||||
{
|
||||
"piiProtection": {
|
||||
"enabled": true,
|
||||
"detectPatterns": ["email", "ssn", "credit_card"],
|
||||
"action": "redact",
|
||||
"auditLog": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## High Availability
|
||||
|
||||
### Load Balancing
|
||||
|
||||
Distribute requests across instances:
|
||||
|
||||
```yaml
|
||||
# HAProxy configuration
|
||||
frontend claude_front
|
||||
bind *:443 ssl crt /etc/ssl/certs/claude.pem
|
||||
default_backend claude_back
|
||||
|
||||
backend claude_back
|
||||
balance roundrobin
|
||||
server claude1 10.0.1.10:8080 check
|
||||
server claude2 10.0.1.11:8080 check
|
||||
server claude3 10.0.1.12:8080 check
|
||||
```
|
||||
|
||||
### Failover
|
||||
|
||||
Automatic failover configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"highAvailability": {
|
||||
"enabled": true,
|
||||
"primaryRegion": "us-east-1",
|
||||
"failoverRegions": ["us-west-2", "eu-west-1"],
|
||||
"healthCheck": {
|
||||
"interval": "30s",
|
||||
"timeout": "5s"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backup & Recovery
|
||||
|
||||
Automated backup strategies:
|
||||
|
||||
```bash
|
||||
# Configure backups
|
||||
claude admin backup configure \
|
||||
--schedule "0 2 * * *" \
|
||||
--retention 30d \
|
||||
--destination s3://backups/claude-code
|
||||
|
||||
# Manual backup
|
||||
claude admin backup create
|
||||
|
||||
# Restore from backup
|
||||
claude admin backup restore backup-20251106
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Network configuration: https://docs.claude.com/claude-code/network-config
|
||||
- Security best practices: `references/best-practices.md`
|
||||
- Monitoring setup: https://docs.claude.com/claude-code/monitoring
|
||||
- Compliance: https://docs.claude.com/claude-code/legal-and-compliance
|
||||
252
skills/claude-code/references/getting-started.md
Normal file
252
skills/claude-code/references/getting-started.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Getting Started with Claude Code
|
||||
|
||||
Installation, authentication, and setup guide for Claude Code.
|
||||
|
||||
## What is Claude Code?
|
||||
|
||||
Claude Code is Anthropic's agentic coding tool that lives in the terminal and helps turn ideas into code faster. Key features:
|
||||
|
||||
- **Agentic Capabilities**: Autonomous planning, execution, and validation
|
||||
- **Terminal Integration**: Works directly in command line
|
||||
- **IDE Support**: Extensions for VS Code and JetBrains IDEs
|
||||
- **Extensibility**: Plugins, skills, slash commands, and MCP servers
|
||||
- **Enterprise Ready**: SSO, sandboxing, monitoring, and compliance features
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **Operating Systems**: macOS, Linux, or Windows (WSL2)
|
||||
- **Runtime**: Node.js 18+ or Python 3.10+
|
||||
- **API Key**: From Anthropic Console (console.anthropic.com)
|
||||
|
||||
### Getting API Key
|
||||
1. Go to console.anthropic.com
|
||||
2. Sign in or create account
|
||||
3. Navigate to API Keys section
|
||||
4. Generate new API key
|
||||
5. Save key securely (cannot be viewed again)
|
||||
|
||||
## Installation
|
||||
|
||||
### Install via npm (Recommended)
|
||||
```bash
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
```
|
||||
|
||||
### Install via pip
|
||||
```bash
|
||||
pip install claude-code
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
```bash
|
||||
claude --version
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### Method 1: Interactive Login
|
||||
```bash
|
||||
claude login
|
||||
# Follow prompts to enter API key
|
||||
```
|
||||
|
||||
### Method 2: Environment Variable
|
||||
```bash
|
||||
# Add to ~/.bashrc or ~/.zshrc
|
||||
export ANTHROPIC_API_KEY=your_api_key_here
|
||||
|
||||
# Or set for single session
|
||||
export ANTHROPIC_API_KEY=your_api_key_here
|
||||
claude
|
||||
```
|
||||
|
||||
### Method 3: Configuration File
|
||||
Create `~/.claude/config.json`:
|
||||
```json
|
||||
{
|
||||
"apiKey": "your_api_key_here"
|
||||
}
|
||||
```
|
||||
|
||||
### Verify Authentication
|
||||
```bash
|
||||
claude "hello"
|
||||
# Should respond without authentication errors
|
||||
```
|
||||
|
||||
## First Run
|
||||
|
||||
### Start Interactive Session
|
||||
```bash
|
||||
# In any directory
|
||||
claude
|
||||
|
||||
# In specific project
|
||||
cd /path/to/project
|
||||
claude
|
||||
```
|
||||
|
||||
### Run with Specific Task
|
||||
```bash
|
||||
claude "implement user authentication"
|
||||
```
|
||||
|
||||
### Run with File Context
|
||||
```bash
|
||||
claude "explain this code" --file app.js
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Interactive Mode
|
||||
```bash
|
||||
$ claude
|
||||
Claude Code> help me create a React component
|
||||
# Claude will plan and execute
|
||||
```
|
||||
|
||||
### One-Shot Mode
|
||||
```bash
|
||||
claude "add error handling to main.py"
|
||||
```
|
||||
|
||||
### With Additional Context
|
||||
```bash
|
||||
claude "refactor this function" --file utils.js --context "make it async"
|
||||
```
|
||||
|
||||
## Understanding the Interface
|
||||
|
||||
### Session Start
|
||||
```
|
||||
Claude Code v1.x.x
|
||||
Working directory: /path/to/project
|
||||
Model: claude-sonnet-4-5-20250929
|
||||
|
||||
Claude Code>
|
||||
```
|
||||
|
||||
### Tool Execution
|
||||
Claude will show:
|
||||
- Tool being used (Read, Write, Bash, etc.)
|
||||
- Tool parameters
|
||||
- Results or outputs
|
||||
- Thinking/planning process (if enabled)
|
||||
|
||||
### Session End
|
||||
```bash
|
||||
# Type Ctrl+C or Ctrl+D
|
||||
# Or type 'exit' or 'quit'
|
||||
```
|
||||
|
||||
## Common First Commands
|
||||
|
||||
### Explore Codebase
|
||||
```bash
|
||||
claude "explain the project structure"
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
```bash
|
||||
claude "run the test suite"
|
||||
```
|
||||
|
||||
### Fix Issues
|
||||
```bash
|
||||
claude "fix all TypeScript errors"
|
||||
```
|
||||
|
||||
### Add Feature
|
||||
```bash
|
||||
claude "add input validation to the login form"
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Claude Code creates `.claude/` in your project:
|
||||
|
||||
```
|
||||
project/
|
||||
├── .claude/
|
||||
│ ├── settings.json # Project-specific settings
|
||||
│ ├── commands/ # Custom slash commands
|
||||
│ ├── skills/ # Custom skills
|
||||
│ ├── hooks.json # Hook configurations
|
||||
│ └── mcp.json # MCP server configurations
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Learn Slash Commands
|
||||
```bash
|
||||
# See available commands
|
||||
/help
|
||||
|
||||
# Try common workflows
|
||||
/cook implement feature X
|
||||
/fix:fast bug in Y
|
||||
/test
|
||||
```
|
||||
|
||||
### Create Custom Skills
|
||||
See `references/agent-skills.md` for creating project-specific skills.
|
||||
|
||||
### Configure MCP Servers
|
||||
See `references/mcp-integration.md` for connecting external tools.
|
||||
|
||||
### Set Up Hooks
|
||||
See `references/hooks-and-plugins.md` for automation.
|
||||
|
||||
### Configure Settings
|
||||
See `references/configuration.md` for customization options.
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
### Authentication Issues
|
||||
```bash
|
||||
# Re-login
|
||||
claude logout
|
||||
claude login
|
||||
|
||||
# Verify API key is set
|
||||
echo $ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la ~/.claude
|
||||
|
||||
# Fix ownership
|
||||
sudo chown -R $USER ~/.claude
|
||||
```
|
||||
|
||||
### Installation Issues
|
||||
```bash
|
||||
# Clear npm cache
|
||||
npm cache clean --force
|
||||
|
||||
# Reinstall
|
||||
npm uninstall -g @anthropic-ai/claude-code
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
```
|
||||
|
||||
### WSL2 Issues (Windows)
|
||||
```bash
|
||||
# Ensure WSL2 is updated
|
||||
wsl --update
|
||||
|
||||
# Check Node.js version in WSL
|
||||
node --version # Should be 18+
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Documentation**: https://docs.claude.com/claude-code
|
||||
- **GitHub Issues**: https://github.com/anthropics/claude-code/issues
|
||||
- **Support**: support.claude.com
|
||||
- **Community**: discord.gg/anthropic
|
||||
|
||||
For detailed troubleshooting, see `references/troubleshooting.md`.
|
||||
443
skills/claude-code/references/hooks-and-plugins.md
Normal file
443
skills/claude-code/references/hooks-and-plugins.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# Hooks and Plugins
|
||||
|
||||
Customize and extend Claude Code behavior with hooks and plugins.
|
||||
|
||||
## Hooks System
|
||||
|
||||
Hooks are shell commands that execute in response to events.
|
||||
|
||||
### Hook Types
|
||||
|
||||
**Pre-tool hooks**: Execute before tool calls
|
||||
**Post-tool hooks**: Execute after tool calls
|
||||
**User prompt submit hooks**: Execute when user submits prompts
|
||||
|
||||
### Configuration
|
||||
|
||||
Hooks are configured in `.claude/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"pre-tool": {
|
||||
"bash": "echo 'Running: $TOOL_ARGS'",
|
||||
"write": "./scripts/validate-write.sh"
|
||||
},
|
||||
"post-tool": {
|
||||
"write": "./scripts/format-code.sh",
|
||||
"edit": "prettier --write $FILE_PATH"
|
||||
},
|
||||
"user-prompt-submit": "./scripts/validate-request.sh"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Available in hook scripts:
|
||||
|
||||
**All hooks:**
|
||||
- `$TOOL_NAME`: Name of the tool being called
|
||||
- `$TOOL_ARGS`: JSON string of tool arguments
|
||||
|
||||
**Post-tool only:**
|
||||
- `$TOOL_RESULT`: Tool execution result
|
||||
|
||||
**User-prompt-submit only:**
|
||||
- `$USER_PROMPT`: User's prompt text
|
||||
|
||||
### Hook Examples
|
||||
|
||||
#### Pre-tool: Security Validation
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .claude/scripts/validate-bash.sh
|
||||
|
||||
# Block dangerous commands
|
||||
if echo "$TOOL_ARGS" | grep -E "rm -rf /|format|mkfs"; then
|
||||
echo "❌ Dangerous command blocked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Command validated"
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"pre-tool": {
|
||||
"bash": "./.claude/scripts/validate-bash.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Post-tool: Auto-format
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .claude/scripts/format-code.sh
|
||||
|
||||
# Extract file path from tool args
|
||||
FILE_PATH=$(echo "$TOOL_ARGS" | jq -r '.file_path')
|
||||
|
||||
# Format based on file type
|
||||
case "$FILE_PATH" in
|
||||
*.js|*.ts|*.jsx|*.tsx)
|
||||
prettier --write "$FILE_PATH"
|
||||
;;
|
||||
*.py)
|
||||
black "$FILE_PATH"
|
||||
;;
|
||||
*.go)
|
||||
gofmt -w "$FILE_PATH"
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"post-tool": {
|
||||
"write": "./.claude/scripts/format-code.sh",
|
||||
"edit": "./.claude/scripts/format-code.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### User-prompt-submit: Cost Tracking
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .claude/scripts/track-usage.sh
|
||||
|
||||
# Log prompt
|
||||
echo "$(date): $USER_PROMPT" >> .claude/usage.log
|
||||
|
||||
# Estimate tokens (rough)
|
||||
TOKEN_COUNT=$(echo "$USER_PROMPT" | wc -w)
|
||||
echo "Estimated tokens: $TOKEN_COUNT"
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"user-prompt-submit": "./.claude/scripts/track-usage.sh"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hook Best Practices
|
||||
|
||||
**Performance**: Keep hooks fast (<100ms)
|
||||
**Reliability**: Handle errors gracefully
|
||||
**Security**: Validate all inputs
|
||||
**Logging**: Log important actions
|
||||
**Testing**: Test hooks thoroughly
|
||||
|
||||
### Hook Errors
|
||||
|
||||
When a hook fails:
|
||||
- Pre-tool hook failure blocks tool execution
|
||||
- Post-tool hook failure is logged but doesn't block
|
||||
- User can configure strict mode to block on all failures
|
||||
|
||||
## Plugins System
|
||||
|
||||
Plugins are packaged collections of extensions.
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── plugin.json # Plugin metadata
|
||||
├── commands/ # Slash commands
|
||||
│ ├── my-command.md
|
||||
│ └── another-command.md
|
||||
├── skills/ # Agent skills
|
||||
│ └── my-skill/
|
||||
│ ├── skill.md
|
||||
│ └── skill.json
|
||||
├── hooks/ # Hook scripts
|
||||
│ ├── hooks.json
|
||||
│ └── scripts/
|
||||
├── mcp/ # MCP server configurations
|
||||
│ └── mcp.json
|
||||
└── README.md # Documentation
|
||||
```
|
||||
|
||||
### plugin.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin description",
|
||||
"author": "Your Name",
|
||||
"homepage": "https://github.com/user/plugin",
|
||||
"license": "MIT",
|
||||
"commands": ["commands/*.md"],
|
||||
"skills": ["skills/*/"],
|
||||
"hooks": "hooks/hooks.json",
|
||||
"mcpServers": "mcp/mcp.json",
|
||||
"dependencies": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Installing Plugins
|
||||
|
||||
#### From GitHub
|
||||
```bash
|
||||
claude plugin install gh:username/repo
|
||||
claude plugin install gh:username/repo@v1.0.0
|
||||
```
|
||||
|
||||
#### From npm
|
||||
```bash
|
||||
claude plugin install npm:package-name
|
||||
claude plugin install npm:@scope/package-name
|
||||
```
|
||||
|
||||
#### From Local Path
|
||||
```bash
|
||||
claude plugin install ./path/to/plugin
|
||||
claude plugin install ~/plugins/my-plugin
|
||||
```
|
||||
|
||||
#### From URL
|
||||
```bash
|
||||
claude plugin install https://example.com/plugin.zip
|
||||
```
|
||||
|
||||
### Managing Plugins
|
||||
|
||||
#### List Installed Plugins
|
||||
```bash
|
||||
claude plugin list
|
||||
```
|
||||
|
||||
#### Update Plugin
|
||||
```bash
|
||||
claude plugin update my-plugin
|
||||
claude plugin update --all
|
||||
```
|
||||
|
||||
#### Uninstall Plugin
|
||||
```bash
|
||||
claude plugin uninstall my-plugin
|
||||
```
|
||||
|
||||
#### Enable/Disable Plugin
|
||||
```bash
|
||||
claude plugin disable my-plugin
|
||||
claude plugin enable my-plugin
|
||||
```
|
||||
|
||||
### Creating Plugins
|
||||
|
||||
#### Initialize Plugin
|
||||
```bash
|
||||
mkdir my-plugin
|
||||
cd my-plugin
|
||||
```
|
||||
|
||||
#### Create plugin.json
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "My awesome plugin",
|
||||
"author": "Your Name",
|
||||
"commands": ["commands/*.md"],
|
||||
"skills": ["skills/*/"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Add Components
|
||||
```bash
|
||||
# Add slash command
|
||||
mkdir -p commands
|
||||
cat > commands/my-command.md <<EOF
|
||||
# My Command
|
||||
Do something awesome with {{input}}.
|
||||
EOF
|
||||
|
||||
# Add skill
|
||||
mkdir -p skills/my-skill
|
||||
cat > skills/my-skill/skill.json <<EOF
|
||||
{
|
||||
"name": "my-skill",
|
||||
"description": "Does something",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Package Plugin
|
||||
```bash
|
||||
# Create archive
|
||||
tar -czf my-plugin.tar.gz .
|
||||
|
||||
# Or zip
|
||||
zip -r my-plugin.zip .
|
||||
```
|
||||
|
||||
### Publishing Plugins
|
||||
|
||||
#### To GitHub
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git tag v1.0.0
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
#### To npm
|
||||
```bash
|
||||
npm init
|
||||
npm publish
|
||||
```
|
||||
|
||||
### Plugin Marketplaces
|
||||
|
||||
Organizations can create private plugin marketplaces.
|
||||
|
||||
#### Configure Marketplace
|
||||
```json
|
||||
{
|
||||
"marketplaces": [
|
||||
{
|
||||
"name": "company-internal",
|
||||
"url": "https://plugins.company.com/catalog.json",
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"token": "${COMPANY_PLUGIN_TOKEN}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Marketplace Catalog Format
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"name": "company-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Internal plugin",
|
||||
"downloadUrl": "https://plugins.company.com/company-plugin-1.0.0.zip",
|
||||
"checksum": "sha256:abc123..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Install from Marketplace
|
||||
```bash
|
||||
claude plugin install company-internal:company-plugin
|
||||
```
|
||||
|
||||
## Example Plugin: Code Quality
|
||||
|
||||
### Structure
|
||||
```
|
||||
code-quality-plugin/
|
||||
├── plugin.json
|
||||
├── commands/
|
||||
│ ├── lint.md
|
||||
│ └── format.md
|
||||
├── skills/
|
||||
│ └── code-review/
|
||||
│ ├── skill.md
|
||||
│ └── skill.json
|
||||
└── hooks/
|
||||
├── hooks.json
|
||||
└── scripts/
|
||||
└── auto-lint.sh
|
||||
```
|
||||
|
||||
### plugin.json
|
||||
```json
|
||||
{
|
||||
"name": "code-quality",
|
||||
"version": "1.0.0",
|
||||
"description": "Code quality tools and automation",
|
||||
"commands": ["commands/*.md"],
|
||||
"skills": ["skills/*/"],
|
||||
"hooks": "hooks/hooks.json"
|
||||
}
|
||||
```
|
||||
|
||||
### commands/lint.md
|
||||
```markdown
|
||||
# Lint
|
||||
|
||||
Run linter on {{files}} and fix all issues automatically.
|
||||
```
|
||||
|
||||
### hooks/hooks.json
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"post-tool": {
|
||||
"write": "./scripts/auto-lint.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Hook Security
|
||||
- Validate all inputs
|
||||
- Use whitelists for allowed commands
|
||||
- Implement timeouts
|
||||
- Log all executions
|
||||
- Review hook scripts regularly
|
||||
|
||||
### Plugin Security
|
||||
- Verify plugin sources
|
||||
- Review code before installation
|
||||
- Use signed packages when available
|
||||
- Monitor plugin behavior
|
||||
- Keep plugins updated
|
||||
|
||||
### Best Practices
|
||||
- Install plugins from trusted sources only
|
||||
- Review plugin permissions
|
||||
- Use plugin sandboxing when available
|
||||
- Monitor resource usage
|
||||
- Regular security audits
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hooks Not Running
|
||||
- Check hooks.json syntax
|
||||
- Verify script permissions (`chmod +x`)
|
||||
- Check script paths
|
||||
- Review logs in `.claude/logs/`
|
||||
|
||||
### Plugin Installation Failures
|
||||
- Verify internet connectivity
|
||||
- Check plugin URL/path
|
||||
- Review error messages
|
||||
- Clear cache: `claude plugin cache clear`
|
||||
|
||||
### Plugin Conflicts
|
||||
- Check for conflicting commands
|
||||
- Review plugin load order
|
||||
- Disable conflicting plugins
|
||||
- Update plugins to compatible versions
|
||||
|
||||
## See Also
|
||||
|
||||
- Creating slash commands: `references/slash-commands.md`
|
||||
- Agent skills: `references/agent-skills.md`
|
||||
- Configuration: `references/configuration.md`
|
||||
- Best practices: `references/best-practices.md`
|
||||
316
skills/claude-code/references/ide-integration.md
Normal file
316
skills/claude-code/references/ide-integration.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# IDE Integration
|
||||
|
||||
Use Claude Code with Visual Studio Code and JetBrains IDEs.
|
||||
|
||||
## Visual Studio Code
|
||||
|
||||
### Installation
|
||||
|
||||
1. Open VS Code
|
||||
2. Go to Extensions (Ctrl+Shift+X)
|
||||
3. Search for "Claude Code"
|
||||
4. Click Install
|
||||
5. Authenticate with API key
|
||||
|
||||
### Features
|
||||
|
||||
**Inline Chat**
|
||||
- Press Ctrl+I (Cmd+I on Mac)
|
||||
- Ask questions about code
|
||||
- Get suggestions in context
|
||||
- Apply changes directly
|
||||
|
||||
**Code Actions**
|
||||
- Right-click on code
|
||||
- Select "Ask Claude"
|
||||
- Get refactoring suggestions
|
||||
- Fix bugs and issues
|
||||
|
||||
**Diff View**
|
||||
- See proposed changes
|
||||
- Accept/reject modifications
|
||||
- Review before applying
|
||||
- Staged diff comparison
|
||||
|
||||
**Terminal Integration**
|
||||
- Built-in Claude terminal
|
||||
- Run commands via Claude
|
||||
- Execute tools directly
|
||||
- View real-time output
|
||||
|
||||
### Configuration
|
||||
|
||||
**.vscode/settings.json:**
|
||||
```json
|
||||
{
|
||||
"claude.apiKey": "${ANTHROPIC_API_KEY}",
|
||||
"claude.model": "claude-sonnet-4-5-20250929",
|
||||
"claude.maxTokens": 8192,
|
||||
"claude.autoSave": true,
|
||||
"claude.inlineChat.enabled": true,
|
||||
"claude.terminalIntegration": true
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
**Default shortcuts:**
|
||||
- `Ctrl+I`: Inline chat
|
||||
- `Ctrl+Shift+C`: Open Claude panel
|
||||
- `Ctrl+Shift+Enter`: Submit to Claude
|
||||
- `Escape`: Close Claude chat
|
||||
|
||||
**Custom shortcuts (.vscode/keybindings.json):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"key": "ctrl+alt+c",
|
||||
"command": "claude.openChat"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+alt+r",
|
||||
"command": "claude.refactor"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Workspace Integration
|
||||
|
||||
**Project-specific Claude settings:**
|
||||
|
||||
.vscode/claude.json:
|
||||
```json
|
||||
{
|
||||
"skills": [".claude/skills/project-skill"],
|
||||
"commands": [".claude/commands"],
|
||||
"mcpServers": ".claude/mcp.json",
|
||||
"outputStyle": "technical-writer"
|
||||
}
|
||||
```
|
||||
|
||||
### Common Workflows
|
||||
|
||||
**Explain Code:**
|
||||
1. Select code
|
||||
2. Right-click → "Ask Claude"
|
||||
3. Type: "Explain this code"
|
||||
|
||||
**Refactor:**
|
||||
1. Select function
|
||||
2. Press Ctrl+I
|
||||
3. Type: "Refactor for better performance"
|
||||
|
||||
**Fix Bug:**
|
||||
1. Click on error
|
||||
2. Press Ctrl+I
|
||||
3. Type: "Fix this error"
|
||||
|
||||
**Generate Tests:**
|
||||
1. Select function
|
||||
2. Right-click → "Ask Claude"
|
||||
3. Type: "Write tests for this"
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
Supported IDEs:
|
||||
- IntelliJ IDEA
|
||||
- PyCharm
|
||||
- WebStorm
|
||||
- PhpStorm
|
||||
- GoLand
|
||||
- RubyMine
|
||||
- CLion
|
||||
- Rider
|
||||
|
||||
### Installation
|
||||
|
||||
1. Open Settings (Ctrl+Alt+S)
|
||||
2. Go to Plugins
|
||||
3. Search "Claude Code"
|
||||
4. Click Install
|
||||
5. Restart IDE
|
||||
6. Authenticate with API key
|
||||
|
||||
### Features
|
||||
|
||||
**AI Assistant Panel**
|
||||
- Dedicated Claude panel
|
||||
- Context-aware suggestions
|
||||
- Multi-file awareness
|
||||
- Project understanding
|
||||
|
||||
**Inline Suggestions**
|
||||
- As-you-type completions
|
||||
- Contextual code generation
|
||||
- Smart refactoring hints
|
||||
- Error fix suggestions
|
||||
|
||||
**Code Reviews**
|
||||
- Automated code reviews
|
||||
- Security vulnerability detection
|
||||
- Best practice recommendations
|
||||
- Performance optimization tips
|
||||
|
||||
**Refactoring Support**
|
||||
- Smart rename
|
||||
- Extract method
|
||||
- Inline variable
|
||||
- Move class
|
||||
|
||||
### Configuration
|
||||
|
||||
**Settings → Tools → Claude Code:**
|
||||
```
|
||||
API Key: [Your API Key]
|
||||
Model: claude-sonnet-4-5-20250929
|
||||
Max Tokens: 8192
|
||||
Auto-complete: Enabled
|
||||
Code Review: Enabled
|
||||
```
|
||||
|
||||
**Project Settings (.idea/claude.xml):**
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ClaudeSettings">
|
||||
<option name="model" value="claude-sonnet-4-5-20250929" />
|
||||
<option name="skillsPath" value=".claude/skills" />
|
||||
<option name="autoReview" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
**Default shortcuts:**
|
||||
- `Ctrl+Shift+A`: Ask Claude
|
||||
- `Alt+Enter`: Quick fixes with Claude
|
||||
- `Ctrl+Alt+L`: Format with Claude suggestions
|
||||
|
||||
**Custom shortcuts (Settings → Keymap → Claude Code):**
|
||||
```
|
||||
Ask Claude: Ctrl+Shift+C
|
||||
Refactor with Claude: Ctrl+Alt+R
|
||||
Generate Tests: Ctrl+Alt+T
|
||||
Code Review: Ctrl+Alt+V
|
||||
```
|
||||
|
||||
### Integration with IDE Features
|
||||
|
||||
**Version Control:**
|
||||
- Review commit diffs with Claude
|
||||
- Generate commit messages
|
||||
- Suggest PR improvements
|
||||
- Analyze merge conflicts
|
||||
|
||||
**Debugger:**
|
||||
- Explain stack traces
|
||||
- Suggest fixes for errors
|
||||
- Debug complex issues
|
||||
- Analyze variable states
|
||||
|
||||
**Database Tools:**
|
||||
- Generate SQL queries
|
||||
- Optimize database schema
|
||||
- Write migration scripts
|
||||
- Explain query plans
|
||||
|
||||
### Common Workflows
|
||||
|
||||
**Generate Boilerplate:**
|
||||
1. Right-click in editor
|
||||
2. Select "Generate" → "Claude Code"
|
||||
3. Choose template type
|
||||
|
||||
**Review Changes:**
|
||||
1. Open Version Control panel
|
||||
2. Right-click on changeset
|
||||
3. Select "Review with Claude"
|
||||
|
||||
**Debug Error:**
|
||||
1. Hit breakpoint
|
||||
2. Right-click in debugger
|
||||
3. Select "Ask Claude about this"
|
||||
|
||||
## CLI Integration
|
||||
|
||||
Use Claude Code from IDE terminal:
|
||||
|
||||
```bash
|
||||
# In VS Code terminal
|
||||
claude "explain this project structure"
|
||||
|
||||
# In JetBrains terminal
|
||||
claude "add error handling to current file"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### VS Code
|
||||
|
||||
**Workspace Organization:**
|
||||
- Use workspace settings for team consistency
|
||||
- Share .vscode/claude.json in version control
|
||||
- Document custom shortcuts
|
||||
- Configure output styles per project
|
||||
|
||||
**Performance:**
|
||||
- Limit inline suggestions in large files
|
||||
- Disable auto-save for better control
|
||||
- Use specific prompts
|
||||
- Close unused editor tabs
|
||||
|
||||
### JetBrains
|
||||
|
||||
**Project Configuration:**
|
||||
- Enable Claude for specific file types only
|
||||
- Configure inspection severity
|
||||
- Set up custom code review templates
|
||||
- Use project-specific skills
|
||||
|
||||
**Performance:**
|
||||
- Adjust auto-complete delay
|
||||
- Limit scope of code analysis
|
||||
- Disable for binary files
|
||||
- Configure memory settings
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### VS Code
|
||||
|
||||
**Extension Not Loading:**
|
||||
```bash
|
||||
# Check extension status
|
||||
code --list-extensions | grep claude
|
||||
|
||||
# Reinstall
|
||||
code --uninstall-extension anthropic.claude-code
|
||||
code --install-extension anthropic.claude-code
|
||||
```
|
||||
|
||||
**Authentication Issues:**
|
||||
- Verify API key in settings
|
||||
- Check environment variable
|
||||
- Re-authenticate in extension
|
||||
- Review proxy settings
|
||||
|
||||
### JetBrains
|
||||
|
||||
**Plugin Not Responding:**
|
||||
```
|
||||
File → Invalidate Caches / Restart
|
||||
Settings → Plugins → Claude Code → Reinstall
|
||||
```
|
||||
|
||||
**Performance Issues:**
|
||||
- Increase IDE memory (Help → Edit Custom VM Options)
|
||||
- Disable unused features
|
||||
- Clear caches
|
||||
- Update plugin version
|
||||
|
||||
## See Also
|
||||
|
||||
- VS Code extension: https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code
|
||||
- JetBrains plugin: https://plugins.jetbrains.com/plugin/claude-code
|
||||
- Configuration: `references/configuration.md`
|
||||
- Troubleshooting: `references/troubleshooting.md`
|
||||
386
skills/claude-code/references/mcp-integration.md
Normal file
386
skills/claude-code/references/mcp-integration.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# MCP Integration
|
||||
|
||||
Model Context Protocol (MCP) integration for connecting Claude Code to external tools and services.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
Model Context Protocol enables Claude Code to:
|
||||
- Connect to external tools and services
|
||||
- Access resources (files, databases, APIs)
|
||||
- Use custom tools
|
||||
- Provide prompts and completions
|
||||
|
||||
## Configuration
|
||||
|
||||
MCP servers are configured in `.claude/mcp.json`:
|
||||
|
||||
### Basic Configuration
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "command-to-run",
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"VAR_NAME": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {}
|
||||
},
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
},
|
||||
"postgres": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-postgres"],
|
||||
"env": {
|
||||
"DATABASE_URL": "postgresql://user:pass@localhost:5432/db"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common MCP Servers
|
||||
|
||||
### Filesystem Access
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"/path/to/allowed/files"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
- Read/write files
|
||||
- List directories
|
||||
- File search
|
||||
- Path restrictions for security
|
||||
|
||||
### GitHub Integration
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
- Repository access
|
||||
- Issues and PRs
|
||||
- Code search
|
||||
- Workflow management
|
||||
|
||||
### PostgreSQL Database
|
||||
```json
|
||||
{
|
||||
"postgres": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-postgres"],
|
||||
"env": {
|
||||
"DATABASE_URL": "${DATABASE_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
- Query execution
|
||||
- Schema inspection
|
||||
- Transaction management
|
||||
- Connection pooling
|
||||
|
||||
### Brave Search
|
||||
```json
|
||||
{
|
||||
"brave-search": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "${BRAVE_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
- Web search
|
||||
- News search
|
||||
- Local search
|
||||
- Result filtering
|
||||
|
||||
### Puppeteer (Browser Automation)
|
||||
```json
|
||||
{
|
||||
"puppeteer": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
- Browser automation
|
||||
- Screenshots
|
||||
- PDF generation
|
||||
- Web scraping
|
||||
|
||||
## Remote MCP Servers
|
||||
|
||||
Connect to MCP servers over HTTP/SSE:
|
||||
|
||||
### Basic Remote Server
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-service": {
|
||||
"url": "https://api.example.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Authentication
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-service": {
|
||||
"url": "https://api.example.com/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${API_TOKEN}",
|
||||
"X-Custom-Header": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Proxy
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-service": {
|
||||
"url": "https://api.example.com/mcp",
|
||||
"proxy": "http://proxy.company.com:8080"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Use environment variables for sensitive data:
|
||||
|
||||
### .env File
|
||||
```bash
|
||||
# .claude/.env
|
||||
GITHUB_TOKEN=ghp_xxxxx
|
||||
DATABASE_URL=postgresql://user:pass@localhost/db
|
||||
BRAVE_API_KEY=BSAxxxxx
|
||||
API_TOKEN=token_xxxxx
|
||||
```
|
||||
|
||||
### Reference in mcp.json
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing MCP Servers
|
||||
|
||||
### Inspector Tool
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector
|
||||
```
|
||||
|
||||
Opens web UI for testing MCP servers:
|
||||
- List available tools
|
||||
- Test tool invocations
|
||||
- View resources
|
||||
- Debug connections
|
||||
|
||||
### Manual Testing
|
||||
```bash
|
||||
# Test server command
|
||||
npx -y @modelcontextprotocol/server-filesystem /tmp
|
||||
|
||||
# Check server output
|
||||
echo '{"jsonrpc":"2.0","method":"initialize","params":{}}' | \
|
||||
npx -y @modelcontextprotocol/server-filesystem /tmp
|
||||
```
|
||||
|
||||
## Creating Custom MCP Servers
|
||||
|
||||
### Python Server
|
||||
```python
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
server = Server("my-server")
|
||||
|
||||
@server.tool()
|
||||
async def my_tool(arg: str) -> str:
|
||||
"""Tool description"""
|
||||
return f"Result: {arg}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
stdio_server(server)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "python",
|
||||
"args": ["path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Node.js Server
|
||||
```javascript
|
||||
import { Server } from "@modelcontextprotocol/server-node";
|
||||
|
||||
const server = new Server("my-server");
|
||||
|
||||
server.tool({
|
||||
name: "my-tool",
|
||||
description: "Tool description",
|
||||
parameters: { arg: "string" }
|
||||
}, async ({ arg }) => {
|
||||
return `Result: ${arg}`;
|
||||
});
|
||||
|
||||
server.listen();
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Filesystem Access
|
||||
- Restrict to specific directories
|
||||
- Use read-only access when possible
|
||||
- Validate file paths
|
||||
- Monitor access logs
|
||||
|
||||
### API Credentials
|
||||
- Use environment variables
|
||||
- Never commit credentials
|
||||
- Rotate keys regularly
|
||||
- Implement least-privilege access
|
||||
|
||||
### Network Access
|
||||
- Whitelist allowed domains
|
||||
- Use HTTPS only
|
||||
- Implement timeouts
|
||||
- Rate limit requests
|
||||
|
||||
### Remote Servers
|
||||
- Validate server certificates
|
||||
- Use authentication
|
||||
- Implement request signing
|
||||
- Monitor for anomalies
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Not Starting
|
||||
```bash
|
||||
# Check server command
|
||||
npx -y @modelcontextprotocol/server-filesystem /tmp
|
||||
|
||||
# Verify environment variables
|
||||
echo $GITHUB_TOKEN
|
||||
|
||||
# Check logs
|
||||
cat ~/.claude/logs/mcp-*.log
|
||||
```
|
||||
|
||||
### Connection Errors
|
||||
```bash
|
||||
# Test network connectivity
|
||||
curl https://api.example.com/mcp
|
||||
|
||||
# Verify proxy settings
|
||||
echo $HTTP_PROXY
|
||||
|
||||
# Check firewall rules
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
```bash
|
||||
# Verify file permissions
|
||||
ls -la /path/to/allowed/files
|
||||
|
||||
# Check user permissions
|
||||
whoami
|
||||
groups
|
||||
```
|
||||
|
||||
### Tool Not Found
|
||||
- Verify server is running
|
||||
- Check server configuration
|
||||
- Inspect server capabilities
|
||||
- Review tool registration
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Configuration Management
|
||||
- Use environment variables for secrets
|
||||
- Document server purposes
|
||||
- Version control mcp.json (without secrets)
|
||||
- Test configurations thoroughly
|
||||
|
||||
### Performance
|
||||
- Use local servers when possible
|
||||
- Implement caching
|
||||
- Set appropriate timeouts
|
||||
- Monitor resource usage
|
||||
|
||||
### Maintenance
|
||||
- Update servers regularly
|
||||
- Monitor server health
|
||||
- Review access logs
|
||||
- Clean up unused servers
|
||||
|
||||
## See Also
|
||||
|
||||
- MCP specification: https://modelcontextprotocol.io
|
||||
- Creating MCP servers: `references/api-reference.md`
|
||||
- Security best practices: `references/best-practices.md`
|
||||
- Troubleshooting: `references/troubleshooting.md`
|
||||
489
skills/claude-code/references/slash-commands.md
Normal file
489
skills/claude-code/references/slash-commands.md
Normal file
@@ -0,0 +1,489 @@
|
||||
# Slash Commands Reference
|
||||
|
||||
Comprehensive catalog of Claude Code slash commands for development workflows.
|
||||
|
||||
## What Are Slash Commands?
|
||||
|
||||
Slash commands are user-defined operations that:
|
||||
- Start with `/` (e.g., `/cook`, `/test`)
|
||||
- Expand to full prompts when executed
|
||||
- Accept arguments
|
||||
- Located in `.claude/commands/`
|
||||
- Can be project-specific or global
|
||||
|
||||
## Development Commands
|
||||
|
||||
### /cook [task]
|
||||
Implement features step by step.
|
||||
|
||||
```bash
|
||||
/cook implement user authentication with JWT
|
||||
/cook add payment integration with Stripe
|
||||
```
|
||||
|
||||
**When to use**: Feature implementation with iterative development
|
||||
|
||||
### /plan [task]
|
||||
Research, analyze, and create implementation plans.
|
||||
|
||||
```bash
|
||||
/plan implement OAuth2 authentication
|
||||
/plan migrate from SQLite to PostgreSQL
|
||||
```
|
||||
|
||||
**When to use**: Before starting complex implementations
|
||||
|
||||
### /debug [issue]
|
||||
Debug technical issues and provide solutions.
|
||||
|
||||
```bash
|
||||
/debug the API returns 500 errors intermittently
|
||||
/debug authentication flow not working
|
||||
```
|
||||
|
||||
**When to use**: Investigating and diagnosing problems
|
||||
|
||||
### /test
|
||||
Run test suite.
|
||||
|
||||
```bash
|
||||
/test
|
||||
```
|
||||
|
||||
**When to use**: Validate implementations, check for regressions
|
||||
|
||||
### /refactor [target]
|
||||
Improve code quality.
|
||||
|
||||
```bash
|
||||
/refactor the authentication module
|
||||
/refactor for better performance
|
||||
```
|
||||
|
||||
**When to use**: Code quality improvements
|
||||
|
||||
## Fix Commands
|
||||
|
||||
### /fix:fast [issue]
|
||||
Quick fixes for small issues.
|
||||
|
||||
```bash
|
||||
/fix:fast the login button is not working
|
||||
/fix:fast typo in error message
|
||||
```
|
||||
|
||||
**When to use**: Simple, straightforward fixes
|
||||
|
||||
### /fix:hard [issue]
|
||||
Complex issues requiring planning and subagents.
|
||||
|
||||
```bash
|
||||
/fix:hard database connection pooling issues
|
||||
/fix:hard race condition in payment processing
|
||||
```
|
||||
|
||||
**When to use**: Complex bugs requiring deep investigation
|
||||
|
||||
### /fix:types
|
||||
Fix TypeScript type errors.
|
||||
|
||||
```bash
|
||||
/fix:types
|
||||
```
|
||||
|
||||
**When to use**: TypeScript compilation errors
|
||||
|
||||
### /fix:test [issue]
|
||||
Fix test failures.
|
||||
|
||||
```bash
|
||||
/fix:test the user service tests are failing
|
||||
/fix:test integration tests timing out
|
||||
```
|
||||
|
||||
**When to use**: Test suite failures
|
||||
|
||||
### /fix:ui [issue]
|
||||
Fix UI issues.
|
||||
|
||||
```bash
|
||||
/fix:ui button alignment on mobile
|
||||
/fix:ui dark mode colors inconsistent
|
||||
```
|
||||
|
||||
**When to use**: Visual or interaction issues
|
||||
|
||||
### /fix:ci [url]
|
||||
Analyze GitHub Actions logs and fix CI/CD issues.
|
||||
|
||||
```bash
|
||||
/fix:ci https://github.com/owner/repo/actions/runs/123456
|
||||
```
|
||||
|
||||
**When to use**: Build or deployment failures
|
||||
|
||||
### /fix:logs [issue]
|
||||
Analyze logs and fix issues.
|
||||
|
||||
```bash
|
||||
/fix:logs server error logs showing memory leaks
|
||||
```
|
||||
|
||||
**When to use**: Production issues with log evidence
|
||||
|
||||
## Documentation Commands
|
||||
|
||||
### /docs:init
|
||||
Create initial documentation structure.
|
||||
|
||||
```bash
|
||||
/docs:init
|
||||
```
|
||||
|
||||
**When to use**: New projects needing documentation
|
||||
|
||||
### /docs:update
|
||||
Update existing documentation based on code changes.
|
||||
|
||||
```bash
|
||||
/docs:update
|
||||
```
|
||||
|
||||
**When to use**: After significant code changes
|
||||
|
||||
### /docs:summarize
|
||||
Summarize codebase and create overview.
|
||||
|
||||
```bash
|
||||
/docs:summarize
|
||||
```
|
||||
|
||||
**When to use**: Generate project summaries
|
||||
|
||||
## Git Commands
|
||||
|
||||
### /git:cm
|
||||
Stage all files and create commit.
|
||||
|
||||
```bash
|
||||
/git:cm
|
||||
```
|
||||
|
||||
**When to use**: Commit changes with automatic message
|
||||
|
||||
### /git:cp
|
||||
Stage, commit, and push all code in current branch.
|
||||
|
||||
```bash
|
||||
/git:cp
|
||||
```
|
||||
|
||||
**When to use**: Commit and push in one command
|
||||
|
||||
### /git:pr [branch] [from-branch]
|
||||
Create pull request.
|
||||
|
||||
```bash
|
||||
/git:pr feature-branch main
|
||||
/git:pr bugfix-auth develop
|
||||
```
|
||||
|
||||
**When to use**: Creating PRs with automatic descriptions
|
||||
|
||||
## Planning Commands
|
||||
|
||||
### /plan:two [task]
|
||||
Create implementation plan with 2 alternative approaches.
|
||||
|
||||
```bash
|
||||
/plan:two implement caching layer
|
||||
```
|
||||
|
||||
**When to use**: Need to evaluate multiple approaches
|
||||
|
||||
### /plan:ci [url]
|
||||
Analyze GitHub Actions logs and create fix plan.
|
||||
|
||||
```bash
|
||||
/plan:ci https://github.com/owner/repo/actions/runs/123456
|
||||
```
|
||||
|
||||
**When to use**: CI/CD failure analysis
|
||||
|
||||
### /plan:cro [issue]
|
||||
Create conversion rate optimization plan.
|
||||
|
||||
```bash
|
||||
/plan:cro landing page conversion improvement
|
||||
```
|
||||
|
||||
**When to use**: Marketing/conversion optimization
|
||||
|
||||
## Content Commands
|
||||
|
||||
### /content:fast [request]
|
||||
Quick copy writing.
|
||||
|
||||
```bash
|
||||
/content:fast write product description for new feature
|
||||
```
|
||||
|
||||
**When to use**: Fast content generation
|
||||
|
||||
### /content:good [request]
|
||||
High-quality, conversion-focused copy.
|
||||
|
||||
```bash
|
||||
/content:good write landing page hero section
|
||||
```
|
||||
|
||||
**When to use**: Marketing copy requiring polish
|
||||
|
||||
### /content:enhance [issue]
|
||||
Enhance existing content.
|
||||
|
||||
```bash
|
||||
/content:enhance improve clarity of pricing page
|
||||
```
|
||||
|
||||
**When to use**: Improving existing copy
|
||||
|
||||
### /content:cro [issue]
|
||||
Conversion rate optimization for content.
|
||||
|
||||
```bash
|
||||
/content:cro optimize email campaign copy
|
||||
```
|
||||
|
||||
**When to use**: Conversion-focused content improvements
|
||||
|
||||
## Design Commands
|
||||
|
||||
### /design:fast [task]
|
||||
Quick design implementation.
|
||||
|
||||
```bash
|
||||
/design:fast create dashboard layout
|
||||
```
|
||||
|
||||
**When to use**: Rapid prototyping
|
||||
|
||||
### /design:good [task]
|
||||
High-quality, polished design.
|
||||
|
||||
```bash
|
||||
/design:good create landing page for SaaS product
|
||||
```
|
||||
|
||||
**When to use**: Production-ready designs
|
||||
|
||||
### /design:3d [task]
|
||||
Create 3D designs with Three.js.
|
||||
|
||||
```bash
|
||||
/design:3d create interactive 3D product viewer
|
||||
```
|
||||
|
||||
**When to use**: 3D visualization needs
|
||||
|
||||
### /design:screenshot [path]
|
||||
Create design based on screenshot.
|
||||
|
||||
```bash
|
||||
/design:screenshot screenshot.png
|
||||
```
|
||||
|
||||
**When to use**: Recreating designs from images
|
||||
|
||||
### /design:video [path]
|
||||
Create design based on video.
|
||||
|
||||
```bash
|
||||
/design:video demo-video.mp4
|
||||
```
|
||||
|
||||
**When to use**: Implementing designs from video demos
|
||||
|
||||
## Deployment Commands
|
||||
|
||||
### /deploy
|
||||
Deploy using deployment tool.
|
||||
|
||||
```bash
|
||||
/deploy
|
||||
```
|
||||
|
||||
**When to use**: Production deployments
|
||||
|
||||
### /deploy-check
|
||||
Check deployment readiness.
|
||||
|
||||
```bash
|
||||
/deploy-check
|
||||
```
|
||||
|
||||
**When to use**: Pre-deployment validation
|
||||
|
||||
## Integration Commands
|
||||
|
||||
### /integrate:polar [tasks]
|
||||
Implement payment integration with Polar.sh.
|
||||
|
||||
```bash
|
||||
/integrate:polar add subscription payments
|
||||
```
|
||||
|
||||
**When to use**: Polar payment integration
|
||||
|
||||
### /integrate:sepay [tasks]
|
||||
Implement payment integration with SePay.vn.
|
||||
|
||||
```bash
|
||||
/integrate:sepay add Vietnamese payment gateway
|
||||
```
|
||||
|
||||
**When to use**: SePay payment integration
|
||||
|
||||
## Other Commands
|
||||
|
||||
### /brainstorm [question]
|
||||
Brainstorm features and ideas.
|
||||
|
||||
```bash
|
||||
/brainstorm how to improve user onboarding
|
||||
```
|
||||
|
||||
**When to use**: Ideation and exploration
|
||||
|
||||
### /ask [question]
|
||||
Answer technical and architectural questions.
|
||||
|
||||
```bash
|
||||
/ask what's the best way to handle websocket connections
|
||||
```
|
||||
|
||||
**When to use**: Technical guidance
|
||||
|
||||
### /scout [prompt] [scale]
|
||||
Scout directories to respond to requests.
|
||||
|
||||
```bash
|
||||
/scout find authentication code
|
||||
```
|
||||
|
||||
**When to use**: Code exploration
|
||||
|
||||
### /watzup
|
||||
Review recent changes and wrap up work.
|
||||
|
||||
```bash
|
||||
/watzup
|
||||
```
|
||||
|
||||
**When to use**: End of session summary
|
||||
|
||||
### /bootstrap [requirements]
|
||||
Bootstrap new project step by step.
|
||||
|
||||
```bash
|
||||
/bootstrap create React app with TypeScript and Tailwind
|
||||
```
|
||||
|
||||
**When to use**: New project setup
|
||||
|
||||
### /bootstrap:auto [requirements]
|
||||
Bootstrap new project automatically.
|
||||
|
||||
```bash
|
||||
/bootstrap:auto create Next.js app
|
||||
```
|
||||
|
||||
**When to use**: Automated project setup
|
||||
|
||||
### /journal
|
||||
Write journal entries for development log.
|
||||
|
||||
```bash
|
||||
/journal
|
||||
```
|
||||
|
||||
**When to use**: Development documentation
|
||||
|
||||
### /review:codebase [prompt]
|
||||
Scan and analyze codebase.
|
||||
|
||||
```bash
|
||||
/review:codebase analyze architecture patterns
|
||||
```
|
||||
|
||||
**When to use**: Codebase analysis
|
||||
|
||||
### /skill:create [prompt]
|
||||
Create new agent skill.
|
||||
|
||||
```bash
|
||||
/skill:create create skill for API testing
|
||||
```
|
||||
|
||||
**When to use**: Extending Claude with custom skills
|
||||
|
||||
## Creating Custom Slash Commands
|
||||
|
||||
### Command File Structure
|
||||
```
|
||||
.claude/commands/
|
||||
└── my-command.md
|
||||
```
|
||||
|
||||
### Example Command File
|
||||
```markdown
|
||||
# File: .claude/commands/my-command.md
|
||||
|
||||
Create comprehensive test suite for {{feature}}.
|
||||
|
||||
Include:
|
||||
- Unit tests
|
||||
- Integration tests
|
||||
- Edge cases
|
||||
- Mocking examples
|
||||
```
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
/my-command authentication
|
||||
# Expands to: "Create comprehensive test suite for authentication..."
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
**Clear prompts**: Write specific, actionable prompts
|
||||
**Use variables**: `{{variable}}` for dynamic content
|
||||
**Document usage**: Add comments explaining the command
|
||||
**Test thoroughly**: Verify commands work as expected
|
||||
|
||||
## Command Arguments
|
||||
|
||||
### Single Argument
|
||||
```bash
|
||||
/cook implement user auth
|
||||
# Argument: "implement user auth"
|
||||
```
|
||||
|
||||
### Multiple Arguments
|
||||
```bash
|
||||
/git:pr feature-branch main
|
||||
# Arguments: "feature-branch", "main"
|
||||
```
|
||||
|
||||
### Optional Arguments
|
||||
Some commands work with or without arguments:
|
||||
```bash
|
||||
/test # Run all tests
|
||||
/test user.test.js # Run specific test
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Creating custom commands: `references/hooks-and-plugins.md`
|
||||
- Command automation: `references/configuration.md`
|
||||
- Best practices: `references/best-practices.md`
|
||||
456
skills/claude-code/references/troubleshooting.md
Normal file
456
skills/claude-code/references/troubleshooting.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# Troubleshooting
|
||||
|
||||
Common issues, debugging, and solutions for Claude Code.
|
||||
|
||||
## Authentication Issues
|
||||
|
||||
### API Key Not Recognized
|
||||
|
||||
**Symptoms:**
|
||||
- "Invalid API key" errors
|
||||
- Authentication failures
|
||||
- 401 Unauthorized responses
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```bash
|
||||
# Verify API key is set
|
||||
echo $ANTHROPIC_API_KEY
|
||||
|
||||
# Re-login
|
||||
claude logout
|
||||
claude login
|
||||
|
||||
# Check API key format (should start with sk-ant-)
|
||||
echo $ANTHROPIC_API_KEY | grep "^sk-ant-"
|
||||
|
||||
# Test API key directly
|
||||
curl https://api.anthropic.com/v1/messages \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{"model":"claude-sonnet-4-5-20250929","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'
|
||||
```
|
||||
|
||||
### Environment Variable Issues
|
||||
|
||||
```bash
|
||||
# Add to shell profile
|
||||
echo 'export ANTHROPIC_API_KEY=sk-ant-xxxxx' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Or use .env file
|
||||
echo 'ANTHROPIC_API_KEY=sk-ant-xxxxx' > .claude/.env
|
||||
|
||||
# Verify it's loaded
|
||||
claude config get apiKey
|
||||
```
|
||||
|
||||
## Installation Problems
|
||||
|
||||
### npm Installation Failures
|
||||
|
||||
```bash
|
||||
# Clear npm cache
|
||||
npm cache clean --force
|
||||
|
||||
# Remove and reinstall
|
||||
npm uninstall -g @anthropic-ai/claude-code
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Use specific version
|
||||
npm install -g @anthropic-ai/claude-code@1.0.0
|
||||
|
||||
# Check installation
|
||||
which claude
|
||||
claude --version
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
|
||||
```bash
|
||||
# Fix permissions on Unix/Mac
|
||||
sudo chown -R $USER ~/.claude
|
||||
chmod -R 755 ~/.claude
|
||||
|
||||
# Or install without sudo (using nvm)
|
||||
nvm install 18
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
```
|
||||
|
||||
### Python Installation Issues
|
||||
|
||||
```bash
|
||||
# Upgrade pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# Install in virtual environment
|
||||
python -m venv claude-env
|
||||
source claude-env/bin/activate
|
||||
pip install claude-code
|
||||
|
||||
# Install with --user flag
|
||||
pip install --user claude-code
|
||||
```
|
||||
|
||||
## Connection & Network Issues
|
||||
|
||||
### Proxy Configuration
|
||||
|
||||
```bash
|
||||
# Set proxy environment variables
|
||||
export HTTP_PROXY=http://proxy.company.com:8080
|
||||
export HTTPS_PROXY=http://proxy.company.com:8080
|
||||
export NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# Configure in settings
|
||||
claude config set proxy http://proxy.company.com:8080
|
||||
|
||||
# Test connection
|
||||
curl -x $HTTP_PROXY https://api.anthropic.com
|
||||
```
|
||||
|
||||
### SSL/TLS Errors
|
||||
|
||||
```bash
|
||||
# Trust custom CA certificate
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-bundle.crt
|
||||
|
||||
# Disable SSL verification (not recommended for production)
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
# Update ca-certificates
|
||||
sudo update-ca-certificates # Debian/Ubuntu
|
||||
sudo update-ca-trust # RHEL/CentOS
|
||||
```
|
||||
|
||||
### Firewall Issues
|
||||
|
||||
```bash
|
||||
# Check connectivity to Anthropic API
|
||||
ping api.anthropic.com
|
||||
telnet api.anthropic.com 443
|
||||
|
||||
# Test HTTPS connection
|
||||
curl -v https://api.anthropic.com
|
||||
|
||||
# Check firewall rules
|
||||
sudo iptables -L # Linux
|
||||
netsh advfirewall show allprofiles # Windows
|
||||
```
|
||||
|
||||
## MCP Server Problems
|
||||
|
||||
### Server Not Starting
|
||||
|
||||
```bash
|
||||
# Test MCP server command manually
|
||||
npx -y @modelcontextprotocol/server-filesystem /tmp
|
||||
|
||||
# Check server logs
|
||||
cat ~/.claude/logs/mcp-*.log
|
||||
|
||||
# Verify environment variables
|
||||
echo $GITHUB_TOKEN # For GitHub MCP
|
||||
|
||||
# Test with MCP Inspector
|
||||
npx @modelcontextprotocol/inspector
|
||||
```
|
||||
|
||||
### Connection Timeouts
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-example"],
|
||||
"timeout": 30000,
|
||||
"retries": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Permission Denied
|
||||
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la /path/to/mcp/server
|
||||
|
||||
# Make executable
|
||||
chmod +x /path/to/mcp/server
|
||||
|
||||
# Check directory access
|
||||
ls -ld /path/to/allowed/directory
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Responses
|
||||
|
||||
**Check network latency:**
|
||||
```bash
|
||||
ping api.anthropic.com
|
||||
```
|
||||
|
||||
**Use faster model:**
|
||||
```bash
|
||||
claude --model haiku "simple task"
|
||||
```
|
||||
|
||||
**Reduce context:**
|
||||
```json
|
||||
{
|
||||
"maxTokens": 4096,
|
||||
"context": {
|
||||
"autoTruncate": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Enable caching:**
|
||||
```json
|
||||
{
|
||||
"caching": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
```bash
|
||||
# Clear cache
|
||||
rm -rf ~/.claude/cache/*
|
||||
|
||||
# Limit context window
|
||||
claude config set maxTokens 8192
|
||||
|
||||
# Disable memory
|
||||
claude config set memory.enabled false
|
||||
|
||||
# Close unused sessions
|
||||
claude session list
|
||||
claude session close session-123
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```bash
|
||||
# Check rate limits
|
||||
claude usage show
|
||||
|
||||
# Wait and retry
|
||||
sleep 60
|
||||
claude "retry task"
|
||||
|
||||
# Implement exponential backoff in scripts
|
||||
```
|
||||
|
||||
## Tool Execution Errors
|
||||
|
||||
### Bash Command Failures
|
||||
|
||||
**Check sandboxing settings:**
|
||||
```json
|
||||
{
|
||||
"sandboxing": {
|
||||
"enabled": true,
|
||||
"allowedPaths": ["/workspace", "/tmp"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verify command permissions:**
|
||||
```bash
|
||||
# Make script executable
|
||||
chmod +x script.sh
|
||||
|
||||
# Check PATH
|
||||
echo $PATH
|
||||
which command-name
|
||||
```
|
||||
|
||||
### File Access Denied
|
||||
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la file.txt
|
||||
|
||||
# Change ownership
|
||||
sudo chown $USER file.txt
|
||||
|
||||
# Grant read/write permissions
|
||||
chmod 644 file.txt
|
||||
```
|
||||
|
||||
### Write Tool Failures
|
||||
|
||||
```bash
|
||||
# Check disk space
|
||||
df -h
|
||||
|
||||
# Verify directory exists
|
||||
mkdir -p /path/to/directory
|
||||
|
||||
# Check write permissions
|
||||
touch /path/to/directory/test.txt
|
||||
rm /path/to/directory/test.txt
|
||||
```
|
||||
|
||||
## Hook Errors
|
||||
|
||||
### Hooks Not Running
|
||||
|
||||
```bash
|
||||
# Verify hooks.json syntax
|
||||
cat .claude/hooks.json | jq .
|
||||
|
||||
# Check hook script permissions
|
||||
chmod +x .claude/scripts/hook.sh
|
||||
|
||||
# Test hook script manually
|
||||
.claude/scripts/hook.sh
|
||||
|
||||
# Check logs
|
||||
cat ~/.claude/logs/hooks.log
|
||||
```
|
||||
|
||||
### Hook Script Errors
|
||||
|
||||
```bash
|
||||
# Add error handling to hooks
|
||||
#!/bin/bash
|
||||
set -e # Exit on error
|
||||
set -u # Exit on undefined variable
|
||||
|
||||
# Debug hook execution
|
||||
#!/bin/bash
|
||||
set -x # Print commands
|
||||
echo "Hook running: $TOOL_NAME"
|
||||
```
|
||||
|
||||
## Debug Mode
|
||||
|
||||
### Enable Debugging
|
||||
|
||||
```bash
|
||||
# Set debug environment variable
|
||||
export CLAUDE_DEBUG=1
|
||||
export CLAUDE_LOG_LEVEL=debug
|
||||
|
||||
# Run with debug flag
|
||||
claude --debug "task"
|
||||
|
||||
# View debug logs
|
||||
tail -f ~/.claude/logs/debug.log
|
||||
```
|
||||
|
||||
### Verbose Output
|
||||
|
||||
```bash
|
||||
# Enable verbose mode
|
||||
claude --verbose "task"
|
||||
|
||||
# Show all tool calls
|
||||
claude --show-tools "task"
|
||||
|
||||
# Display thinking process
|
||||
claude --show-thinking "task"
|
||||
```
|
||||
|
||||
## Common Error Messages
|
||||
|
||||
### "Model not found"
|
||||
|
||||
```bash
|
||||
# Use correct model name
|
||||
claude --model claude-sonnet-4-5-20250929
|
||||
|
||||
# Update claude-code
|
||||
npm update -g @anthropic-ai/claude-code
|
||||
```
|
||||
|
||||
### "Rate limit exceeded"
|
||||
|
||||
```bash
|
||||
# Wait and retry
|
||||
sleep 60
|
||||
|
||||
# Check usage
|
||||
claude usage show
|
||||
|
||||
# Implement rate limiting in code
|
||||
```
|
||||
|
||||
### "Context length exceeded"
|
||||
|
||||
```bash
|
||||
# Reduce context
|
||||
claude config set maxTokens 100000
|
||||
|
||||
# Summarize long content
|
||||
claude "summarize this codebase"
|
||||
|
||||
# Process in chunks
|
||||
claude "analyze first half of files"
|
||||
```
|
||||
|
||||
### "Timeout waiting for response"
|
||||
|
||||
```bash
|
||||
# Increase timeout
|
||||
claude config set timeout 300
|
||||
|
||||
# Check network connection
|
||||
ping api.anthropic.com
|
||||
|
||||
# Retry with smaller request
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Collect Diagnostic Info
|
||||
|
||||
```bash
|
||||
# System info
|
||||
claude --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
# Configuration
|
||||
claude config list --all
|
||||
|
||||
# Recent logs
|
||||
tail -n 100 ~/.claude/logs/session.log
|
||||
|
||||
# Environment
|
||||
env | grep CLAUDE
|
||||
env | grep ANTHROPIC
|
||||
```
|
||||
|
||||
### Report Issues
|
||||
|
||||
1. **Check existing issues**: https://github.com/anthropics/claude-code/issues
|
||||
2. **Gather diagnostic info**
|
||||
3. **Create minimal reproduction**
|
||||
4. **Submit issue** with:
|
||||
- Claude Code version
|
||||
- Operating system
|
||||
- Error messages
|
||||
- Steps to reproduce
|
||||
|
||||
### Support Channels
|
||||
|
||||
- **Documentation**: https://docs.claude.com/claude-code
|
||||
- **GitHub Issues**: https://github.com/anthropics/claude-code/issues
|
||||
- **Support Portal**: support.claude.com
|
||||
- **Community Discord**: discord.gg/anthropic
|
||||
|
||||
## See Also
|
||||
|
||||
- Installation guide: `references/getting-started.md`
|
||||
- Configuration: `references/configuration.md`
|
||||
- MCP setup: `references/mcp-integration.md`
|
||||
- Best practices: `references/best-practices.md`
|
||||
6
skills/claude-code/skill.json
Normal file
6
skills/claude-code/skill.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "claude-code",
|
||||
"description": "Use when users ask about Claude Code features, setup, configuration, troubleshooting, slash commands, MCP servers, Agent Skills, hooks, plugins, CI/CD integration, or enterprise deployment. Activate for questions like 'How do I use Claude Code?', 'What slash commands are available?', 'How to set up MCP?', 'Create a skill', 'Fix Claude Code issues', or 'Deploy Claude Code in enterprise'.",
|
||||
"version": "2.0.0",
|
||||
"author": "ClaudeKit Engineer"
|
||||
}
|
||||
140
skills/code-review/SKILL.md
Normal file
140
skills/code-review/SKILL.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Use when receiving code review feedback (especially if unclear or technically questionable), when completing tasks or major features requiring review before proceeding, or before making any completion/success claims. Covers three practices - receiving feedback with technical rigor over performative agreement, requesting reviews via code-reviewer subagent, and verification gates requiring evidence before any status claims. Essential for subagent-driven development, pull requests, and preventing false completion claims.
|
||||
---
|
||||
|
||||
# Code Review
|
||||
|
||||
Guide proper code review practices emphasizing technical rigor, evidence-based claims, and verification over performative responses.
|
||||
|
||||
## Overview
|
||||
|
||||
Code review requires three distinct practices:
|
||||
|
||||
1. **Receiving feedback** - Technical evaluation over performative agreement
|
||||
2. **Requesting reviews** - Systematic review via code-reviewer subagent
|
||||
3. **Verification gates** - Evidence before any completion claims
|
||||
|
||||
Each practice has specific triggers and protocols detailed in reference files.
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Technical correctness over social comfort.** Verify before implementing. Ask before assuming. Evidence before claims.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
### Receiving Feedback
|
||||
Trigger when:
|
||||
- Receiving code review comments from any source
|
||||
- Feedback seems unclear or technically questionable
|
||||
- Multiple review items need prioritization
|
||||
- External reviewer lacks full context
|
||||
- Suggestion conflicts with existing decisions
|
||||
|
||||
**Reference:** `references/code-review-reception.md`
|
||||
|
||||
### Requesting Review
|
||||
Trigger when:
|
||||
- Completing tasks in subagent-driven development (after EACH task)
|
||||
- Finishing major features or refactors
|
||||
- Before merging to main branch
|
||||
- Stuck and need fresh perspective
|
||||
- After fixing complex bugs
|
||||
|
||||
**Reference:** `references/requesting-code-review.md`
|
||||
|
||||
### Verification Gates
|
||||
Trigger when:
|
||||
- About to claim tests pass, build succeeds, or work is complete
|
||||
- Before committing, pushing, or creating PRs
|
||||
- Moving to next task
|
||||
- Any statement suggesting success/completion
|
||||
- Expressing satisfaction with work
|
||||
|
||||
**Reference:** `references/verification-before-completion.md`
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
SITUATION?
|
||||
│
|
||||
├─ Received feedback
|
||||
│ ├─ Unclear items? → STOP, ask for clarification first
|
||||
│ ├─ From human partner? → Understand, then implement
|
||||
│ └─ From external reviewer? → Verify technically before implementing
|
||||
│
|
||||
├─ Completed work
|
||||
│ ├─ Major feature/task? → Request code-reviewer subagent review
|
||||
│ └─ Before merge? → Request code-reviewer subagent review
|
||||
│
|
||||
└─ About to claim status
|
||||
├─ Have fresh verification? → State claim WITH evidence
|
||||
└─ No fresh verification? → RUN verification command first
|
||||
```
|
||||
|
||||
## Receiving Feedback Protocol
|
||||
|
||||
### Response Pattern
|
||||
READ → UNDERSTAND → VERIFY → EVALUATE → RESPOND → IMPLEMENT
|
||||
|
||||
### Key Rules
|
||||
- ❌ No performative agreement: "You're absolutely right!", "Great point!", "Thanks for [anything]"
|
||||
- ❌ No implementation before verification
|
||||
- ✅ Restate requirement, ask questions, push back with technical reasoning, or just start working
|
||||
- ✅ If unclear: STOP and ask for clarification on ALL unclear items first
|
||||
- ✅ YAGNI check: grep for usage before implementing suggested "proper" features
|
||||
|
||||
### Source Handling
|
||||
- **Human partner:** Trusted - implement after understanding, no performative agreement
|
||||
- **External reviewers:** Verify technically correct, check for breakage, push back if wrong
|
||||
|
||||
**Full protocol:** `references/code-review-reception.md`
|
||||
|
||||
## Requesting Review Protocol
|
||||
|
||||
### When to Request
|
||||
- After each task in subagent-driven development
|
||||
- After major feature completion
|
||||
- Before merge to main
|
||||
|
||||
### Process
|
||||
1. Get git SHAs: `BASE_SHA=$(git rev-parse HEAD~1)` and `HEAD_SHA=$(git rev-parse HEAD)`
|
||||
2. Dispatch code-reviewer subagent via Task tool with: WHAT_WAS_IMPLEMENTED, PLAN_OR_REQUIREMENTS, BASE_SHA, HEAD_SHA, DESCRIPTION
|
||||
3. Act on feedback: Fix Critical immediately, Important before proceeding, note Minor for later
|
||||
|
||||
**Full protocol:** `references/requesting-code-review.md`
|
||||
|
||||
## Verification Gates Protocol
|
||||
|
||||
### The Iron Law
|
||||
**NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE**
|
||||
|
||||
### Gate Function
|
||||
IDENTIFY command → RUN full command → READ output → VERIFY confirms claim → THEN claim
|
||||
|
||||
Skip any step = lying, not verifying
|
||||
|
||||
### Requirements
|
||||
- Tests pass: Test output shows 0 failures
|
||||
- Build succeeds: Build command exit 0
|
||||
- Bug fixed: Test original symptom passes
|
||||
- Requirements met: Line-by-line checklist verified
|
||||
|
||||
### Red Flags - STOP
|
||||
Using "should"/"probably"/"seems to", expressing satisfaction before verification, committing without verification, trusting agent reports, ANY wording implying success without running verification
|
||||
|
||||
**Full protocol:** `references/verification-before-completion.md`
|
||||
|
||||
## Integration with Workflows
|
||||
|
||||
- **Subagent-Driven:** Review after EACH task, verify before moving to next
|
||||
- **Pull Requests:** Verify tests pass, request code-reviewer review before merge
|
||||
- **General:** Apply verification gates before any status claims, push back on invalid feedback
|
||||
|
||||
## Bottom Line
|
||||
|
||||
1. Technical rigor over social performance - No performative agreement
|
||||
2. Systematic review processes - Use code-reviewer subagent
|
||||
3. Evidence before claims - Verification gates always
|
||||
|
||||
Verify. Question. Then implement. Evidence. Then claim.
|
||||
209
skills/code-review/references/code-review-reception.md
Normal file
209
skills/code-review/references/code-review-reception.md
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
name: receiving-code-review
|
||||
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
||||
---
|
||||
|
||||
# Code Review Reception
|
||||
|
||||
## Overview
|
||||
|
||||
Code review requires technical evaluation, not emotional performance.
|
||||
|
||||
**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
|
||||
|
||||
## The Response Pattern
|
||||
|
||||
```
|
||||
WHEN receiving code review feedback:
|
||||
|
||||
1. READ: Complete feedback without reacting
|
||||
2. UNDERSTAND: Restate requirement in own words (or ask)
|
||||
3. VERIFY: Check against codebase reality
|
||||
4. EVALUATE: Technically sound for THIS codebase?
|
||||
5. RESPOND: Technical acknowledgment or reasoned pushback
|
||||
6. IMPLEMENT: One item at a time, test each
|
||||
```
|
||||
|
||||
## Forbidden Responses
|
||||
|
||||
**NEVER:**
|
||||
- "You're absolutely right!" (explicit CLAUDE.md violation)
|
||||
- "Great point!" / "Excellent feedback!" (performative)
|
||||
- "Let me implement that now" (before verification)
|
||||
|
||||
**INSTEAD:**
|
||||
- Restate the technical requirement
|
||||
- Ask clarifying questions
|
||||
- Push back with technical reasoning if wrong
|
||||
- Just start working (actions > words)
|
||||
|
||||
## Handling Unclear Feedback
|
||||
|
||||
```
|
||||
IF any item is unclear:
|
||||
STOP - do not implement anything yet
|
||||
ASK for clarification on unclear items
|
||||
|
||||
WHY: Items may be related. Partial understanding = wrong implementation.
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
your human partner: "Fix 1-6"
|
||||
You understand 1,2,3,6. Unclear on 4,5.
|
||||
|
||||
❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
|
||||
✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding."
|
||||
```
|
||||
|
||||
## Source-Specific Handling
|
||||
|
||||
### From your human partner
|
||||
- **Trusted** - implement after understanding
|
||||
- **Still ask** if scope unclear
|
||||
- **No performative agreement**
|
||||
- **Skip to action** or technical acknowledgment
|
||||
|
||||
### From External Reviewers
|
||||
```
|
||||
BEFORE implementing:
|
||||
1. Check: Technically correct for THIS codebase?
|
||||
2. Check: Breaks existing functionality?
|
||||
3. Check: Reason for current implementation?
|
||||
4. Check: Works on all platforms/versions?
|
||||
5. Check: Does reviewer understand full context?
|
||||
|
||||
IF suggestion seems wrong:
|
||||
Push back with technical reasoning
|
||||
|
||||
IF can't easily verify:
|
||||
Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
|
||||
|
||||
IF conflicts with your human partner's prior decisions:
|
||||
Stop and discuss with your human partner first
|
||||
```
|
||||
|
||||
**your human partner's rule:** "External feedback - be skeptical, but check carefully"
|
||||
|
||||
## YAGNI Check for "Professional" Features
|
||||
|
||||
```
|
||||
IF reviewer suggests "implementing properly":
|
||||
grep codebase for actual usage
|
||||
|
||||
IF unused: "This endpoint isn't called. Remove it (YAGNI)?"
|
||||
IF used: Then implement properly
|
||||
```
|
||||
|
||||
**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
FOR multi-item feedback:
|
||||
1. Clarify anything unclear FIRST
|
||||
2. Then implement in this order:
|
||||
- Blocking issues (breaks, security)
|
||||
- Simple fixes (typos, imports)
|
||||
- Complex fixes (refactoring, logic)
|
||||
3. Test each fix individually
|
||||
4. Verify no regressions
|
||||
```
|
||||
|
||||
## When To Push Back
|
||||
|
||||
Push back when:
|
||||
- Suggestion breaks existing functionality
|
||||
- Reviewer lacks full context
|
||||
- Violates YAGNI (unused feature)
|
||||
- Technically incorrect for this stack
|
||||
- Legacy/compatibility reasons exist
|
||||
- Conflicts with your human partner's architectural decisions
|
||||
|
||||
**How to push back:**
|
||||
- Use technical reasoning, not defensiveness
|
||||
- Ask specific questions
|
||||
- Reference working tests/code
|
||||
- Involve your human partner if architectural
|
||||
|
||||
**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"
|
||||
|
||||
## Acknowledging Correct Feedback
|
||||
|
||||
When feedback IS correct:
|
||||
```
|
||||
✅ "Fixed. [Brief description of what changed]"
|
||||
✅ "Good catch - [specific issue]. Fixed in [location]."
|
||||
✅ [Just fix it and show in the code]
|
||||
|
||||
❌ "You're absolutely right!"
|
||||
❌ "Great point!"
|
||||
❌ "Thanks for catching that!"
|
||||
❌ "Thanks for [anything]"
|
||||
❌ ANY gratitude expression
|
||||
```
|
||||
|
||||
**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback.
|
||||
|
||||
**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead.
|
||||
|
||||
## Gracefully Correcting Your Pushback
|
||||
|
||||
If you pushed back and were wrong:
|
||||
```
|
||||
✅ "You were right - I checked [X] and it does [Y]. Implementing now."
|
||||
✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing."
|
||||
|
||||
❌ Long apology
|
||||
❌ Defending why you pushed back
|
||||
❌ Over-explaining
|
||||
```
|
||||
|
||||
State the correction factually and move on.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Performative agreement | State requirement or just act |
|
||||
| Blind implementation | Verify against codebase first |
|
||||
| Batch without testing | One at a time, test each |
|
||||
| Assuming reviewer is right | Check if breaks things |
|
||||
| Avoiding pushback | Technical correctness > comfort |
|
||||
| Partial implementation | Clarify all items first |
|
||||
| Can't verify, proceed anyway | State limitation, ask for direction |
|
||||
|
||||
## Real Examples
|
||||
|
||||
**Performative Agreement (Bad):**
|
||||
```
|
||||
Reviewer: "Remove legacy code"
|
||||
❌ "You're absolutely right! Let me remove that..."
|
||||
```
|
||||
|
||||
**Technical Verification (Good):**
|
||||
```
|
||||
Reviewer: "Remove legacy code"
|
||||
✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?"
|
||||
```
|
||||
|
||||
**YAGNI (Good):**
|
||||
```
|
||||
Reviewer: "Implement proper metrics tracking with database, date filters, CSV export"
|
||||
✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?"
|
||||
```
|
||||
|
||||
**Unclear Item (Good):**
|
||||
```
|
||||
your human partner: "Fix items 1-6"
|
||||
You understand 1,2,3,6. Unclear on 4,5.
|
||||
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
|
||||
```
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**External feedback = suggestions to evaluate, not orders to follow.**
|
||||
|
||||
Verify. Question. Then implement.
|
||||
|
||||
No performative agreement. Technical rigor always.
|
||||
105
skills/code-review/references/requesting-code-review.md
Normal file
105
skills/code-review/references/requesting-code-review.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: requesting-code-review
|
||||
description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements - dispatches code-reviewer subagent to review implementation against plan or requirements before proceeding
|
||||
---
|
||||
|
||||
# Requesting Code Review
|
||||
|
||||
Dispatch code-reviewer subagent to catch issues before they cascade.
|
||||
|
||||
**Core principle:** Review early, review often.
|
||||
|
||||
## When to Request Review
|
||||
|
||||
**Mandatory:**
|
||||
- After each task in subagent-driven development
|
||||
- After completing major feature
|
||||
- Before merge to main
|
||||
|
||||
**Optional but valuable:**
|
||||
- When stuck (fresh perspective)
|
||||
- Before refactoring (baseline check)
|
||||
- After fixing complex bug
|
||||
|
||||
## How to Request
|
||||
|
||||
**1. Get git SHAs:**
|
||||
```bash
|
||||
BASE_SHA=$(git rev-parse HEAD~1) # or origin/main
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
**2. Dispatch code-reviewer subagent:**
|
||||
|
||||
Use Task tool with `code-reviewer` type, fill template at `code-reviewer.md`
|
||||
|
||||
**Placeholders:**
|
||||
- `{WHAT_WAS_IMPLEMENTED}` - What you just built
|
||||
- `{PLAN_OR_REQUIREMENTS}` - What it should do
|
||||
- `{BASE_SHA}` - Starting commit
|
||||
- `{HEAD_SHA}` - Ending commit
|
||||
- `{DESCRIPTION}` - Brief summary
|
||||
|
||||
**3. Act on feedback:**
|
||||
- Fix Critical issues immediately
|
||||
- Fix Important issues before proceeding
|
||||
- Note Minor issues for later
|
||||
- Push back if reviewer is wrong (with reasoning)
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
[Just completed Task 2: Add verification function]
|
||||
|
||||
You: Let me request code review before proceeding.
|
||||
|
||||
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
|
||||
[Dispatch code-reviewer subagent]
|
||||
WHAT_WAS_IMPLEMENTED: Verification and repair functions for conversation index
|
||||
PLAN_OR_REQUIREMENTS: Task 2 from docs/plans/deployment-plan.md
|
||||
BASE_SHA: a7981ec
|
||||
HEAD_SHA: 3df7661
|
||||
DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types
|
||||
|
||||
[Subagent returns]:
|
||||
Strengths: Clean architecture, real tests
|
||||
Issues:
|
||||
Important: Missing progress indicators
|
||||
Minor: Magic number (100) for reporting interval
|
||||
Assessment: Ready to proceed
|
||||
|
||||
You: [Fix progress indicators]
|
||||
[Continue to Task 3]
|
||||
```
|
||||
|
||||
## Integration with Workflows
|
||||
|
||||
**Subagent-Driven Development:**
|
||||
- Review after EACH task
|
||||
- Catch issues before they compound
|
||||
- Fix before moving to next task
|
||||
|
||||
**Executing Plans:**
|
||||
- Review after each batch (3 tasks)
|
||||
- Get feedback, apply, continue
|
||||
|
||||
**Ad-Hoc Development:**
|
||||
- Review before merge
|
||||
- Review when stuck
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Skip review because "it's simple"
|
||||
- Ignore Critical issues
|
||||
- Proceed with unfixed Important issues
|
||||
- Argue with valid technical feedback
|
||||
|
||||
**If reviewer wrong:**
|
||||
- Push back with technical reasoning
|
||||
- Show code/tests that prove it works
|
||||
- Request clarification
|
||||
|
||||
See template at: requesting-code-review/code-reviewer.md
|
||||
139
skills/code-review/references/verification-before-completion.md
Normal file
139
skills/code-review/references/verification-before-completion.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: verification-before-completion
|
||||
description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
|
||||
---
|
||||
|
||||
# Verification Before Completion
|
||||
|
||||
## Overview
|
||||
|
||||
Claiming work is complete without verification is dishonesty, not efficiency.
|
||||
|
||||
**Core principle:** Evidence before claims, always.
|
||||
|
||||
**Violating the letter of this rule is violating the spirit of this rule.**
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE
|
||||
```
|
||||
|
||||
If you haven't run the verification command in this message, you cannot claim it passes.
|
||||
|
||||
## The Gate Function
|
||||
|
||||
```
|
||||
BEFORE claiming any status or expressing satisfaction:
|
||||
|
||||
1. IDENTIFY: What command proves this claim?
|
||||
2. RUN: Execute the FULL command (fresh, complete)
|
||||
3. READ: Full output, check exit code, count failures
|
||||
4. VERIFY: Does output confirm the claim?
|
||||
- If NO: State actual status with evidence
|
||||
- If YES: State claim WITH evidence
|
||||
5. ONLY THEN: Make the claim
|
||||
|
||||
Skip any step = lying, not verifying
|
||||
```
|
||||
|
||||
## Common Failures
|
||||
|
||||
| Claim | Requires | Not Sufficient |
|
||||
|-------|----------|----------------|
|
||||
| Tests pass | Test command output: 0 failures | Previous run, "should pass" |
|
||||
| Linter clean | Linter output: 0 errors | Partial check, extrapolation |
|
||||
| Build succeeds | Build command: exit 0 | Linter passing, logs look good |
|
||||
| Bug fixed | Test original symptom: passes | Code changed, assumed fixed |
|
||||
| Regression test works | Red-green cycle verified | Test passes once |
|
||||
| Agent completed | VCS diff shows changes | Agent reports "success" |
|
||||
| Requirements met | Line-by-line checklist | Tests passing |
|
||||
|
||||
## Red Flags - STOP
|
||||
|
||||
- Using "should", "probably", "seems to"
|
||||
- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!", etc.)
|
||||
- About to commit/push/PR without verification
|
||||
- Trusting agent success reports
|
||||
- Relying on partial verification
|
||||
- Thinking "just this once"
|
||||
- Tired and wanting work over
|
||||
- **ANY wording implying success without having run verification**
|
||||
|
||||
## Rationalization Prevention
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Should work now" | RUN the verification |
|
||||
| "I'm confident" | Confidence ≠ evidence |
|
||||
| "Just this once" | No exceptions |
|
||||
| "Linter passed" | Linter ≠ compiler |
|
||||
| "Agent said success" | Verify independently |
|
||||
| "I'm tired" | Exhaustion ≠ excuse |
|
||||
| "Partial check is enough" | Partial proves nothing |
|
||||
| "Different words so rule doesn't apply" | Spirit over letter |
|
||||
|
||||
## Key Patterns
|
||||
|
||||
**Tests:**
|
||||
```
|
||||
✅ [Run test command] [See: 34/34 pass] "All tests pass"
|
||||
❌ "Should pass now" / "Looks correct"
|
||||
```
|
||||
|
||||
**Regression tests (TDD Red-Green):**
|
||||
```
|
||||
✅ Write → Run (pass) → Revert fix → Run (MUST FAIL) → Restore → Run (pass)
|
||||
❌ "I've written a regression test" (without red-green verification)
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```
|
||||
✅ [Run build] [See: exit 0] "Build passes"
|
||||
❌ "Linter passed" (linter doesn't check compilation)
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
```
|
||||
✅ Re-read plan → Create checklist → Verify each → Report gaps or completion
|
||||
❌ "Tests pass, phase complete"
|
||||
```
|
||||
|
||||
**Agent delegation:**
|
||||
```
|
||||
✅ Agent reports success → Check VCS diff → Verify changes → Report actual state
|
||||
❌ Trust agent report
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
From 24 failure memories:
|
||||
- your human partner said "I don't believe you" - trust broken
|
||||
- Undefined functions shipped - would crash
|
||||
- Missing requirements shipped - incomplete features
|
||||
- Time wasted on false completion → redirect → rework
|
||||
- Violates: "Honesty is a core value. If you lie, you'll be replaced."
|
||||
|
||||
## When To Apply
|
||||
|
||||
**ALWAYS before:**
|
||||
- ANY variation of success/completion claims
|
||||
- ANY expression of satisfaction
|
||||
- ANY positive statement about work state
|
||||
- Committing, PR creation, task completion
|
||||
- Moving to next task
|
||||
- Delegating to agents
|
||||
|
||||
**Rule applies to:**
|
||||
- Exact phrases
|
||||
- Paraphrases and synonyms
|
||||
- Implications of success
|
||||
- ANY communication suggesting completion/correctness
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**No shortcuts for verification.**
|
||||
|
||||
Run the command. Read the output. THEN claim the result.
|
||||
|
||||
This is non-negotiable.
|
||||
120
skills/common/README.md
Normal file
120
skills/common/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Common Skill Utilities
|
||||
|
||||
This directory contains shared utilities used across multiple skills.
|
||||
|
||||
## API Key Helper
|
||||
|
||||
`api_key_helper.py` provides standardized configuration for all Gemini-based skills, supporting both Google AI Studio and Vertex AI endpoints.
|
||||
|
||||
### Usage in Skills
|
||||
|
||||
```python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add common directory to path
|
||||
common_dir = Path(__file__).parent.parent.parent / 'common'
|
||||
sys.path.insert(0, str(common_dir))
|
||||
|
||||
from api_key_helper import get_api_key_or_exit
|
||||
|
||||
# Get API key with automatic error handling
|
||||
api_key = get_api_key_or_exit()
|
||||
```
|
||||
|
||||
### API Key Lookup Order
|
||||
|
||||
The helper checks for `GEMINI_API_KEY` in this order:
|
||||
|
||||
1. **Process environment variable** (recommended for development)
|
||||
```bash
|
||||
export GEMINI_API_KEY='your-api-key'
|
||||
```
|
||||
|
||||
2. **Project root `.env` file**
|
||||
```bash
|
||||
echo 'GEMINI_API_KEY=your-api-key' > .env
|
||||
```
|
||||
|
||||
3. **.claude/.env file**
|
||||
```bash
|
||||
echo 'GEMINI_API_KEY=your-api-key' > .claude/.env
|
||||
```
|
||||
|
||||
4. **.claude/skills/.env file** (shared across all Gemini skills)
|
||||
```bash
|
||||
echo 'GEMINI_API_KEY=your-api-key' > .claude/skills/.env
|
||||
```
|
||||
|
||||
5. **Skill directory `.env` file**
|
||||
```bash
|
||||
echo 'GEMINI_API_KEY=your-api-key' > .claude/skills/your-skill/.env
|
||||
```
|
||||
|
||||
### Vertex AI Support
|
||||
|
||||
To use Vertex AI instead of Google AI Studio:
|
||||
|
||||
```bash
|
||||
# Enable Vertex AI
|
||||
export GEMINI_USE_VERTEX=true
|
||||
export VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
export VERTEX_LOCATION=us-central1 # Optional, defaults to us-central1
|
||||
```
|
||||
|
||||
Or in `.env` file:
|
||||
```
|
||||
GEMINI_USE_VERTEX=true
|
||||
VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
VERTEX_LOCATION=us-central1
|
||||
```
|
||||
|
||||
### Using get_client() Helper
|
||||
|
||||
For automatic client selection (AI Studio or Vertex AI):
|
||||
|
||||
```python
|
||||
from api_key_helper import get_client
|
||||
|
||||
# Get appropriate client based on configuration
|
||||
client_info = get_client()
|
||||
|
||||
if client_info['type'] == 'vertex':
|
||||
# Using Vertex AI
|
||||
from vertexai.generative_models import GenerativeModel
|
||||
model = GenerativeModel('gemini-2.5-flash')
|
||||
response = model.generate_content("Hello")
|
||||
else:
|
||||
# Using AI Studio
|
||||
client = client_info['client']
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
contents="Hello"
|
||||
)
|
||||
```
|
||||
|
||||
### Using get_vertex_config() Helper
|
||||
|
||||
For checking Vertex AI configuration:
|
||||
|
||||
```python
|
||||
from api_key_helper import get_vertex_config
|
||||
|
||||
vertex_config = get_vertex_config()
|
||||
if vertex_config['use_vertex']:
|
||||
print(f"Using Vertex AI")
|
||||
print(f"Project: {vertex_config['project_id']}")
|
||||
print(f"Location: {vertex_config['location']}")
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
If the API key is not found, the helper will:
|
||||
- Print a clear error message
|
||||
- Show all available methods to set the API key
|
||||
- Provide the URL to obtain an API key
|
||||
- Exit with status code 1
|
||||
|
||||
For Vertex AI, if `VERTEX_PROJECT_ID` is missing when `GEMINI_USE_VERTEX=true`, the helper will provide clear instructions.
|
||||
|
||||
This ensures users get immediate, actionable feedback when configuration is missing.
|
||||
300
skills/common/api_key_helper.py
Normal file
300
skills/common/api_key_helper.py
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common API Key Detection Helper for Gemini Skills
|
||||
|
||||
Supports both Google AI Studio and Vertex AI endpoints.
|
||||
|
||||
API Key Detection Order:
|
||||
1. Process environment variable
|
||||
2. Project root .env file
|
||||
3. ./.claude/.env
|
||||
4. ./.claude/skills/.env
|
||||
5. Skill directory .env file
|
||||
|
||||
Vertex AI Configuration:
|
||||
- GEMINI_USE_VERTEX: Set to "true" to use Vertex AI endpoint
|
||||
- VERTEX_PROJECT_ID: GCP project ID (required for Vertex AI)
|
||||
- VERTEX_LOCATION: GCP region (default: us-central1)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
def find_api_key(skill_dir: Optional[Path] = None) -> Optional[str]:
|
||||
"""
|
||||
Find GEMINI_API_KEY using 5-step lookup:
|
||||
1. Process environment
|
||||
2. Project root .env
|
||||
3. ./.claude/.env
|
||||
4. ./.claude/skills/.env
|
||||
5. Skill directory .env
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory (optional, auto-detected if None)
|
||||
|
||||
Returns:
|
||||
API key string or None if not found
|
||||
"""
|
||||
# Step 1: Check process environment
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if api_key:
|
||||
print("✓ Using API key from environment variable", file=sys.stderr)
|
||||
return api_key
|
||||
|
||||
# Determine paths
|
||||
if skill_dir is None:
|
||||
skill_dir = Path(__file__).parent.parent
|
||||
project_dir = skill_dir.parent.parent.parent # 3 levels up from skill dir
|
||||
|
||||
# Step 2: Check project root .env
|
||||
project_env = project_dir / '.env'
|
||||
if project_env.exists():
|
||||
api_key = load_env_file(project_env)
|
||||
if api_key:
|
||||
print(f"✓ Using API key from {project_env}", file=sys.stderr)
|
||||
return api_key
|
||||
|
||||
# Step 3: Check ./.claude/.env
|
||||
claude_env = project_dir / '.claude' / '.env'
|
||||
if claude_env.exists():
|
||||
api_key = load_env_file(claude_env)
|
||||
if api_key:
|
||||
print(f"✓ Using API key from {claude_env}", file=sys.stderr)
|
||||
return api_key
|
||||
|
||||
# Step 4: Check ./.claude/skills/.env
|
||||
claude_skills_env = project_dir / '.claude' / 'skills' / '.env'
|
||||
if claude_skills_env.exists():
|
||||
api_key = load_env_file(claude_skills_env)
|
||||
if api_key:
|
||||
print(f"✓ Using API key from {claude_skills_env}", file=sys.stderr)
|
||||
return api_key
|
||||
|
||||
# Step 5: Check skill directory .env
|
||||
skill_env = skill_dir / '.env'
|
||||
if skill_env.exists():
|
||||
api_key = load_env_file(skill_env)
|
||||
if api_key:
|
||||
print(f"✓ Using API key from {skill_env}", file=sys.stderr)
|
||||
return api_key
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def load_env_file(env_path: Path) -> Optional[str]:
|
||||
"""
|
||||
Load GEMINI_API_KEY from .env file
|
||||
|
||||
Args:
|
||||
env_path: Path to .env file
|
||||
|
||||
Returns:
|
||||
API key or None
|
||||
"""
|
||||
try:
|
||||
with open(env_path, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith('GEMINI_API_KEY='):
|
||||
# Extract value, removing quotes if present
|
||||
value = line.split('=', 1)[1].strip()
|
||||
value = value.strip('"').strip("'")
|
||||
if value:
|
||||
return value
|
||||
except Exception as e:
|
||||
print(f"Warning: Error reading {env_path}: {e}", file=sys.stderr)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def load_env_var(env_path: Path, var_name: str) -> Optional[str]:
|
||||
"""
|
||||
Load a specific environment variable from .env file
|
||||
|
||||
Args:
|
||||
env_path: Path to .env file
|
||||
var_name: Name of the environment variable
|
||||
|
||||
Returns:
|
||||
Variable value or None
|
||||
"""
|
||||
try:
|
||||
with open(env_path, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith(f'{var_name}='):
|
||||
value = line.split('=', 1)[1].strip()
|
||||
value = value.strip('"').strip("'")
|
||||
if value:
|
||||
return value
|
||||
except Exception as e:
|
||||
print(f"Warning: Error reading {env_path}: {e}", file=sys.stderr)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_env_var(var_name: str, skill_dir: Optional[Path] = None) -> Optional[str]:
|
||||
"""
|
||||
Find environment variable using 5-step lookup (same as API key)
|
||||
|
||||
Args:
|
||||
var_name: Name of environment variable
|
||||
skill_dir: Path to skill directory (optional)
|
||||
|
||||
Returns:
|
||||
Variable value or None
|
||||
"""
|
||||
# Step 1: Check process environment
|
||||
value = os.getenv(var_name)
|
||||
if value:
|
||||
return value
|
||||
|
||||
# Determine paths
|
||||
if skill_dir is None:
|
||||
skill_dir = Path(__file__).parent.parent
|
||||
project_dir = skill_dir.parent.parent.parent
|
||||
|
||||
# Step 2-5: Check .env files in order
|
||||
env_files = [
|
||||
project_dir / '.env',
|
||||
project_dir / '.claude' / '.env',
|
||||
project_dir / '.claude' / 'skills' / '.env',
|
||||
skill_dir / '.env'
|
||||
]
|
||||
|
||||
for env_path in env_files:
|
||||
if env_path.exists():
|
||||
value = load_env_var(env_path, var_name)
|
||||
if value:
|
||||
return value
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_vertex_config(skill_dir: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get Vertex AI configuration from environment variables
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory (optional)
|
||||
|
||||
Returns:
|
||||
Dictionary with Vertex AI configuration:
|
||||
{
|
||||
'use_vertex': bool,
|
||||
'project_id': str or None,
|
||||
'location': str (default: 'us-central1')
|
||||
}
|
||||
"""
|
||||
use_vertex_str = find_env_var('GEMINI_USE_VERTEX', skill_dir)
|
||||
use_vertex = use_vertex_str and use_vertex_str.lower() in ('true', '1', 'yes')
|
||||
|
||||
config = {
|
||||
'use_vertex': use_vertex,
|
||||
'project_id': find_env_var('VERTEX_PROJECT_ID', skill_dir) if use_vertex else None,
|
||||
'location': find_env_var('VERTEX_LOCATION', skill_dir) or 'us-central1'
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_api_key_or_exit(skill_dir: Optional[Path] = None) -> str:
|
||||
"""
|
||||
Get API key or exit with helpful error message
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory (optional, auto-detected if None)
|
||||
|
||||
Returns:
|
||||
API key string
|
||||
"""
|
||||
api_key = find_api_key(skill_dir)
|
||||
|
||||
if not api_key:
|
||||
print("\n❌ Error: GEMINI_API_KEY not found!", file=sys.stderr)
|
||||
print("\n📋 Please set your API key using one of these methods (in priority order):", file=sys.stderr)
|
||||
|
||||
if skill_dir is None:
|
||||
skill_dir = Path(__file__).parent.parent
|
||||
project_dir = skill_dir.parent.parent.parent
|
||||
|
||||
print("\n1️⃣ Environment variable (recommended for development):", file=sys.stderr)
|
||||
print(" export GEMINI_API_KEY='your-api-key'", file=sys.stderr)
|
||||
|
||||
print("\n2️⃣ Project root .env file:", file=sys.stderr)
|
||||
print(f" echo 'GEMINI_API_KEY=your-api-key' > {project_dir}/.env", file=sys.stderr)
|
||||
|
||||
print("\n3️⃣ .claude/.env file:", file=sys.stderr)
|
||||
print(f" echo 'GEMINI_API_KEY=your-api-key' > {project_dir}/.claude/.env", file=sys.stderr)
|
||||
|
||||
print("\n4️⃣ .claude/skills/.env file (shared across all Gemini skills):", file=sys.stderr)
|
||||
print(f" echo 'GEMINI_API_KEY=your-api-key' > {project_dir}/.claude/skills/.env", file=sys.stderr)
|
||||
|
||||
print("\n5️⃣ Skill directory .env file:", file=sys.stderr)
|
||||
print(f" echo 'GEMINI_API_KEY=your-api-key' > {skill_dir}/.env", file=sys.stderr)
|
||||
|
||||
print("\n🔑 Get your API key at: https://aistudio.google.com/apikey", file=sys.stderr)
|
||||
print("\n💡 Tip: Add .env files to .gitignore to avoid committing API keys", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return api_key
|
||||
|
||||
|
||||
def get_client(skill_dir: Optional[Path] = None):
|
||||
"""
|
||||
Get appropriate Gemini client (AI Studio or Vertex AI)
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory (optional)
|
||||
|
||||
Returns:
|
||||
genai.Client or vertexai client
|
||||
"""
|
||||
vertex_config = get_vertex_config(skill_dir)
|
||||
|
||||
if vertex_config['use_vertex']:
|
||||
# Use Vertex AI
|
||||
import vertexai
|
||||
from vertexai.generative_models import GenerativeModel
|
||||
|
||||
if not vertex_config['project_id']:
|
||||
print("\n❌ Error: VERTEX_PROJECT_ID required when GEMINI_USE_VERTEX=true!", file=sys.stderr)
|
||||
print("\n📋 Set your GCP project ID:", file=sys.stderr)
|
||||
print(" export VERTEX_PROJECT_ID='your-project-id'", file=sys.stderr)
|
||||
print(" Or add to .env file: VERTEX_PROJECT_ID=your-project-id", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✓ Using Vertex AI endpoint", file=sys.stderr)
|
||||
print(f" Project: {vertex_config['project_id']}", file=sys.stderr)
|
||||
print(f" Location: {vertex_config['location']}", file=sys.stderr)
|
||||
|
||||
vertexai.init(
|
||||
project=vertex_config['project_id'],
|
||||
location=vertex_config['location']
|
||||
)
|
||||
|
||||
return {'type': 'vertex', 'config': vertex_config}
|
||||
else:
|
||||
# Use AI Studio
|
||||
from google import genai
|
||||
|
||||
api_key = get_api_key_or_exit(skill_dir)
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
return {'type': 'aistudio', 'client': client}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Test the API key detection
|
||||
api_key = get_api_key_or_exit()
|
||||
print(f"✓ Found API key: {api_key[:8]}..." + "*" * (len(api_key) - 8))
|
||||
|
||||
# Test Vertex AI config
|
||||
vertex_config = get_vertex_config()
|
||||
if vertex_config['use_vertex']:
|
||||
print(f"\n✓ Vertex AI enabled:")
|
||||
print(f" Project: {vertex_config['project_id']}")
|
||||
print(f" Location: {vertex_config['location']}")
|
||||
232
skills/databases/SKILL.md
Normal file
232
skills/databases/SKILL.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
name: databases
|
||||
description: Work with MongoDB (document database, BSON documents, aggregation pipelines, Atlas cloud) and PostgreSQL (relational database, SQL queries, psql CLI, pgAdmin). Use when designing database schemas, writing queries and aggregations, optimizing indexes for performance, performing database migrations, configuring replication and sharding, implementing backup and restore strategies, managing database users and permissions, analyzing query performance, or administering production databases.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Databases Skill
|
||||
|
||||
Unified guide for working with MongoDB (document-oriented) and PostgreSQL (relational) databases. Choose the right database for your use case and master both systems.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when:
|
||||
- Designing database schemas and data models
|
||||
- Writing queries (SQL or MongoDB query language)
|
||||
- Building aggregation pipelines or complex joins
|
||||
- Optimizing indexes and query performance
|
||||
- Implementing database migrations
|
||||
- Setting up replication, sharding, or clustering
|
||||
- Configuring backups and disaster recovery
|
||||
- Managing database users and permissions
|
||||
- Analyzing slow queries and performance issues
|
||||
- Administering production database deployments
|
||||
|
||||
## Database Selection Guide
|
||||
|
||||
### Choose MongoDB When:
|
||||
- Schema flexibility: frequent structure changes, heterogeneous data
|
||||
- Document-centric: natural JSON/BSON data model
|
||||
- Horizontal scaling: need to shard across multiple servers
|
||||
- High write throughput: IoT, logging, real-time analytics
|
||||
- Nested/hierarchical data: embedded documents preferred
|
||||
- Rapid prototyping: schema evolution without migrations
|
||||
|
||||
**Best for:** Content management, catalogs, IoT time series, real-time analytics, mobile apps, user profiles
|
||||
|
||||
### Choose PostgreSQL When:
|
||||
- Strong consistency: ACID transactions critical
|
||||
- Complex relationships: many-to-many joins, referential integrity
|
||||
- SQL requirement: team expertise, reporting tools, BI systems
|
||||
- Data integrity: strict schema validation, constraints
|
||||
- Mature ecosystem: extensive tooling, extensions
|
||||
- Complex queries: window functions, CTEs, analytical workloads
|
||||
|
||||
**Best for:** Financial systems, e-commerce transactions, ERP, CRM, data warehousing, analytics
|
||||
|
||||
### Both Support:
|
||||
- JSON/JSONB storage and querying
|
||||
- Full-text search capabilities
|
||||
- Geospatial queries and indexing
|
||||
- Replication and high availability
|
||||
- ACID transactions (MongoDB 4.0+)
|
||||
- Strong security features
|
||||
|
||||
## Quick Start
|
||||
|
||||
### MongoDB Setup
|
||||
|
||||
```bash
|
||||
# Atlas (Cloud) - Recommended
|
||||
# 1. Sign up at mongodb.com/atlas
|
||||
# 2. Create M0 free cluster
|
||||
# 3. Get connection string
|
||||
|
||||
# Connection
|
||||
mongodb+srv://user:pass@cluster.mongodb.net/db
|
||||
|
||||
# Shell
|
||||
mongosh "mongodb+srv://cluster.mongodb.net/mydb"
|
||||
|
||||
# Basic operations
|
||||
db.users.insertOne({ name: "Alice", age: 30 })
|
||||
db.users.find({ age: { $gte: 18 } })
|
||||
db.users.updateOne({ name: "Alice" }, { $set: { age: 31 } })
|
||||
db.users.deleteOne({ name: "Alice" })
|
||||
```
|
||||
|
||||
### PostgreSQL Setup
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install postgresql postgresql-contrib
|
||||
|
||||
# Start service
|
||||
sudo systemctl start postgresql
|
||||
|
||||
# Connect
|
||||
psql -U postgres -d mydb
|
||||
|
||||
# Basic operations
|
||||
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, age INT);
|
||||
INSERT INTO users (name, age) VALUES ('Alice', 30);
|
||||
SELECT * FROM users WHERE age >= 18;
|
||||
UPDATE users SET age = 31 WHERE name = 'Alice';
|
||||
DELETE FROM users WHERE name = 'Alice';
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Create/Insert
|
||||
```javascript
|
||||
// MongoDB
|
||||
db.users.insertOne({ name: "Bob", email: "bob@example.com" })
|
||||
db.users.insertMany([{ name: "Alice" }, { name: "Charlie" }])
|
||||
```
|
||||
|
||||
```sql
|
||||
-- PostgreSQL
|
||||
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
||||
INSERT INTO users (name, email) VALUES ('Alice', NULL), ('Charlie', NULL);
|
||||
```
|
||||
|
||||
### Read/Query
|
||||
```javascript
|
||||
// MongoDB
|
||||
db.users.find({ age: { $gte: 18 } })
|
||||
db.users.findOne({ email: "bob@example.com" })
|
||||
```
|
||||
|
||||
```sql
|
||||
-- PostgreSQL
|
||||
SELECT * FROM users WHERE age >= 18;
|
||||
SELECT * FROM users WHERE email = 'bob@example.com' LIMIT 1;
|
||||
```
|
||||
|
||||
### Update
|
||||
```javascript
|
||||
// MongoDB
|
||||
db.users.updateOne({ name: "Bob" }, { $set: { age: 25 } })
|
||||
db.users.updateMany({ status: "pending" }, { $set: { status: "active" } })
|
||||
```
|
||||
|
||||
```sql
|
||||
-- PostgreSQL
|
||||
UPDATE users SET age = 25 WHERE name = 'Bob';
|
||||
UPDATE users SET status = 'active' WHERE status = 'pending';
|
||||
```
|
||||
|
||||
### Delete
|
||||
```javascript
|
||||
// MongoDB
|
||||
db.users.deleteOne({ name: "Bob" })
|
||||
db.users.deleteMany({ status: "deleted" })
|
||||
```
|
||||
|
||||
```sql
|
||||
-- PostgreSQL
|
||||
DELETE FROM users WHERE name = 'Bob';
|
||||
DELETE FROM users WHERE status = 'deleted';
|
||||
```
|
||||
|
||||
### Indexing
|
||||
```javascript
|
||||
// MongoDB
|
||||
db.users.createIndex({ email: 1 })
|
||||
db.users.createIndex({ status: 1, createdAt: -1 })
|
||||
```
|
||||
|
||||
```sql
|
||||
-- PostgreSQL
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_status_created ON users(status, created_at DESC);
|
||||
```
|
||||
|
||||
## Reference Navigation
|
||||
|
||||
### MongoDB References
|
||||
- **[mongodb-crud.md](references/mongodb-crud.md)** - CRUD operations, query operators, atomic updates
|
||||
- **[mongodb-aggregation.md](references/mongodb-aggregation.md)** - Aggregation pipeline, stages, operators, patterns
|
||||
- **[mongodb-indexing.md](references/mongodb-indexing.md)** - Index types, compound indexes, performance optimization
|
||||
- **[mongodb-atlas.md](references/mongodb-atlas.md)** - Atlas cloud setup, clusters, monitoring, search
|
||||
|
||||
### PostgreSQL References
|
||||
- **[postgresql-queries.md](references/postgresql-queries.md)** - SELECT, JOINs, subqueries, CTEs, window functions
|
||||
- **[postgresql-psql-cli.md](references/postgresql-psql-cli.md)** - psql commands, meta-commands, scripting
|
||||
- **[postgresql-performance.md](references/postgresql-performance.md)** - EXPLAIN, query optimization, vacuum, indexes
|
||||
- **[postgresql-administration.md](references/postgresql-administration.md)** - User management, backups, replication, maintenance
|
||||
|
||||
## Python Utilities
|
||||
|
||||
Database utility scripts in `scripts/`:
|
||||
- **db_migrate.py** - Generate and apply migrations for both databases
|
||||
- **db_backup.py** - Backup and restore MongoDB and PostgreSQL
|
||||
- **db_performance_check.py** - Analyze slow queries and recommend indexes
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
python scripts/db_migrate.py --db mongodb --generate "add_user_index"
|
||||
|
||||
# Run backup
|
||||
python scripts/db_backup.py --db postgres --output /backups/
|
||||
|
||||
# Check performance
|
||||
python scripts/db_performance_check.py --db mongodb --threshold 100ms
|
||||
```
|
||||
|
||||
## Key Differences Summary
|
||||
|
||||
| Feature | MongoDB | PostgreSQL |
|
||||
|---------|---------|------------|
|
||||
| Data Model | Document (JSON/BSON) | Relational (Tables/Rows) |
|
||||
| Schema | Flexible, dynamic | Strict, predefined |
|
||||
| Query Language | MongoDB Query Language | SQL |
|
||||
| Joins | $lookup (limited) | Native, optimized |
|
||||
| Transactions | Multi-document (4.0+) | Native ACID |
|
||||
| Scaling | Horizontal (sharding) | Vertical (primary), Horizontal (extensions) |
|
||||
| Indexes | Single, compound, text, geo, etc | B-tree, hash, GiST, GIN, etc |
|
||||
|
||||
## Best Practices
|
||||
|
||||
**MongoDB:**
|
||||
- Use embedded documents for 1-to-few relationships
|
||||
- Reference documents for 1-to-many or many-to-many
|
||||
- Index frequently queried fields
|
||||
- Use aggregation pipeline for complex transformations
|
||||
- Enable authentication and TLS in production
|
||||
- Use Atlas for managed hosting
|
||||
|
||||
**PostgreSQL:**
|
||||
- Normalize schema to 3NF, denormalize for performance
|
||||
- Use foreign keys for referential integrity
|
||||
- Index foreign keys and frequently filtered columns
|
||||
- Use EXPLAIN ANALYZE to optimize queries
|
||||
- Regular VACUUM and ANALYZE maintenance
|
||||
- Connection pooling (pgBouncer) for web apps
|
||||
|
||||
## Resources
|
||||
|
||||
- MongoDB: https://www.mongodb.com/docs/
|
||||
- PostgreSQL: https://www.postgresql.org/docs/
|
||||
- MongoDB University: https://learn.mongodb.com/
|
||||
- PostgreSQL Tutorial: https://www.postgresqltutorial.com/
|
||||
447
skills/databases/references/mongodb-aggregation.md
Normal file
447
skills/databases/references/mongodb-aggregation.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# MongoDB Aggregation Pipeline
|
||||
|
||||
Aggregation pipeline for complex data transformations, analytics, and multi-stage processing.
|
||||
|
||||
## Pipeline Concept
|
||||
|
||||
Aggregation processes documents through multiple stages. Each stage transforms documents and passes results to next stage.
|
||||
|
||||
```javascript
|
||||
db.collection.aggregate([
|
||||
{ /* Stage 1 */ },
|
||||
{ /* Stage 2 */ },
|
||||
{ /* Stage 3 */ }
|
||||
])
|
||||
```
|
||||
|
||||
## Core Pipeline Stages
|
||||
|
||||
### $match (Filter Documents)
|
||||
```javascript
|
||||
// Filter early in pipeline for efficiency
|
||||
db.orders.aggregate([
|
||||
{ $match: { status: "completed", total: { $gte: 100 } } },
|
||||
// Subsequent stages process only matched documents
|
||||
])
|
||||
|
||||
// Multiple conditions
|
||||
db.orders.aggregate([
|
||||
{ $match: {
|
||||
$and: [
|
||||
{ orderDate: { $gte: startDate } },
|
||||
{ status: { $in: ["completed", "shipped"] } }
|
||||
]
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### $project (Reshape Documents)
|
||||
```javascript
|
||||
// Select and reshape fields
|
||||
db.orders.aggregate([
|
||||
{ $project: {
|
||||
orderNumber: 1,
|
||||
total: 1,
|
||||
customerName: "$customer.name",
|
||||
year: { $year: "$orderDate" },
|
||||
_id: 0 // Exclude _id
|
||||
}}
|
||||
])
|
||||
|
||||
// Computed fields
|
||||
db.orders.aggregate([
|
||||
{ $project: {
|
||||
total: 1,
|
||||
tax: { $multiply: ["$total", 0.1] },
|
||||
grandTotal: { $add: ["$total", { $multiply: ["$total", 0.1] }] }
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### $group (Aggregate Data)
|
||||
```javascript
|
||||
// Group and count
|
||||
db.orders.aggregate([
|
||||
{ $group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 }
|
||||
}}
|
||||
])
|
||||
|
||||
// Multiple aggregations
|
||||
db.orders.aggregate([
|
||||
{ $group: {
|
||||
_id: "$customerId",
|
||||
totalSpent: { $sum: "$total" },
|
||||
orderCount: { $sum: 1 },
|
||||
avgOrderValue: { $avg: "$total" },
|
||||
maxOrder: { $max: "$total" },
|
||||
minOrder: { $min: "$total" }
|
||||
}}
|
||||
])
|
||||
|
||||
// Group by multiple fields
|
||||
db.sales.aggregate([
|
||||
{ $group: {
|
||||
_id: {
|
||||
year: { $year: "$date" },
|
||||
month: { $month: "$date" },
|
||||
product: "$productId"
|
||||
},
|
||||
revenue: { $sum: "$amount" }
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### $sort (Order Results)
|
||||
```javascript
|
||||
// Sort by field
|
||||
db.orders.aggregate([
|
||||
{ $sort: { total: -1 } } // -1: descending, 1: ascending
|
||||
])
|
||||
|
||||
// Sort by multiple fields
|
||||
db.orders.aggregate([
|
||||
{ $sort: { status: 1, orderDate: -1 } }
|
||||
])
|
||||
```
|
||||
|
||||
### $limit / $skip (Pagination)
|
||||
```javascript
|
||||
// Limit results
|
||||
db.orders.aggregate([
|
||||
{ $sort: { orderDate: -1 } },
|
||||
{ $limit: 10 }
|
||||
])
|
||||
|
||||
// Pagination
|
||||
const page = 2;
|
||||
const pageSize = 20;
|
||||
db.orders.aggregate([
|
||||
{ $sort: { orderDate: -1 } },
|
||||
{ $skip: (page - 1) * pageSize },
|
||||
{ $limit: pageSize }
|
||||
])
|
||||
```
|
||||
|
||||
### $lookup (Join Collections)
|
||||
```javascript
|
||||
// Simple join
|
||||
db.orders.aggregate([
|
||||
{ $lookup: {
|
||||
from: "customers",
|
||||
localField: "customerId",
|
||||
foreignField: "_id",
|
||||
as: "customer"
|
||||
}},
|
||||
{ $unwind: "$customer" } // Convert array to object
|
||||
])
|
||||
|
||||
// Pipeline join (more powerful)
|
||||
db.orders.aggregate([
|
||||
{ $lookup: {
|
||||
from: "products",
|
||||
let: { items: "$items" },
|
||||
pipeline: [
|
||||
{ $match: { $expr: { $in: ["$_id", "$$items.productId"] } } },
|
||||
{ $project: { name: 1, price: 1 } }
|
||||
],
|
||||
as: "productDetails"
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### $unwind (Deconstruct Arrays)
|
||||
```javascript
|
||||
// Unwind array field
|
||||
db.orders.aggregate([
|
||||
{ $unwind: "$items" }
|
||||
])
|
||||
|
||||
// Preserve null/empty arrays
|
||||
db.orders.aggregate([
|
||||
{ $unwind: {
|
||||
path: "$items",
|
||||
preserveNullAndEmptyArrays: true
|
||||
}}
|
||||
])
|
||||
|
||||
// Include array index
|
||||
db.orders.aggregate([
|
||||
{ $unwind: {
|
||||
path: "$items",
|
||||
includeArrayIndex: "itemIndex"
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### $addFields (Add New Fields)
|
||||
```javascript
|
||||
// Add computed fields
|
||||
db.orders.aggregate([
|
||||
{ $addFields: {
|
||||
totalWithTax: { $multiply: ["$total", 1.1] },
|
||||
year: { $year: "$orderDate" }
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### $replaceRoot (Replace Document Root)
|
||||
```javascript
|
||||
// Promote subdocument to root
|
||||
db.orders.aggregate([
|
||||
{ $replaceRoot: { newRoot: "$customer" } }
|
||||
])
|
||||
|
||||
// Merge fields
|
||||
db.orders.aggregate([
|
||||
{ $replaceRoot: {
|
||||
newRoot: { $mergeObjects: ["$customer", { orderId: "$_id" }] }
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
## Aggregation Operators
|
||||
|
||||
### Arithmetic Operators
|
||||
```javascript
|
||||
// Basic math
|
||||
db.products.aggregate([
|
||||
{ $project: {
|
||||
name: 1,
|
||||
profit: { $subtract: ["$price", "$cost"] },
|
||||
margin: { $multiply: [
|
||||
{ $divide: [
|
||||
{ $subtract: ["$price", "$cost"] },
|
||||
"$price"
|
||||
]},
|
||||
100
|
||||
]}
|
||||
}}
|
||||
])
|
||||
|
||||
// Other operators: $add, $multiply, $divide, $mod, $abs, $ceil, $floor, $round
|
||||
```
|
||||
|
||||
### String Operators
|
||||
```javascript
|
||||
// String manipulation
|
||||
db.users.aggregate([
|
||||
{ $project: {
|
||||
fullName: { $concat: ["$firstName", " ", "$lastName"] },
|
||||
email: { $toLower: "$email" },
|
||||
initials: { $concat: [
|
||||
{ $substr: ["$firstName", 0, 1] },
|
||||
{ $substr: ["$lastName", 0, 1] }
|
||||
]}
|
||||
}}
|
||||
])
|
||||
|
||||
// Other: $toUpper, $trim, $split, $substr, $regexMatch
|
||||
```
|
||||
|
||||
### Date Operators
|
||||
```javascript
|
||||
// Date extraction
|
||||
db.events.aggregate([
|
||||
{ $project: {
|
||||
event: 1,
|
||||
year: { $year: "$timestamp" },
|
||||
month: { $month: "$timestamp" },
|
||||
day: { $dayOfMonth: "$timestamp" },
|
||||
hour: { $hour: "$timestamp" },
|
||||
dayOfWeek: { $dayOfWeek: "$timestamp" }
|
||||
}}
|
||||
])
|
||||
|
||||
// Date math
|
||||
db.events.aggregate([
|
||||
{ $project: {
|
||||
event: 1,
|
||||
expiresAt: { $add: ["$createdAt", 1000 * 60 * 60 * 24 * 30] }, // +30 days
|
||||
ageInDays: { $divide: [
|
||||
{ $subtract: [new Date(), "$createdAt"] },
|
||||
1000 * 60 * 60 * 24
|
||||
]}
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### Array Operators
|
||||
```javascript
|
||||
// Array operations
|
||||
db.posts.aggregate([
|
||||
{ $project: {
|
||||
title: 1,
|
||||
tagCount: { $size: "$tags" },
|
||||
firstTag: { $arrayElemAt: ["$tags", 0] },
|
||||
lastTag: { $arrayElemAt: ["$tags", -1] },
|
||||
hasMongoDBTag: { $in: ["mongodb", "$tags"] }
|
||||
}}
|
||||
])
|
||||
|
||||
// Array filtering
|
||||
db.posts.aggregate([
|
||||
{ $project: {
|
||||
title: 1,
|
||||
activeTags: {
|
||||
$filter: {
|
||||
input: "$tags",
|
||||
as: "tag",
|
||||
cond: { $ne: ["$$tag.status", "deprecated"] }
|
||||
}
|
||||
}
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### Conditional Operators
|
||||
```javascript
|
||||
// $cond (ternary)
|
||||
db.products.aggregate([
|
||||
{ $project: {
|
||||
name: 1,
|
||||
status: {
|
||||
$cond: {
|
||||
if: { $gte: ["$stock", 10] },
|
||||
then: "In Stock",
|
||||
else: "Low Stock"
|
||||
}
|
||||
}
|
||||
}}
|
||||
])
|
||||
|
||||
// $switch (multiple conditions)
|
||||
db.orders.aggregate([
|
||||
{ $project: {
|
||||
status: 1,
|
||||
priority: {
|
||||
$switch: {
|
||||
branches: [
|
||||
{ case: { $gte: ["$total", 1000] }, then: "High" },
|
||||
{ case: { $gte: ["$total", 100] }, then: "Medium" }
|
||||
],
|
||||
default: "Low"
|
||||
}
|
||||
}
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Time-Based Aggregation
|
||||
```javascript
|
||||
// Daily sales
|
||||
db.orders.aggregate([
|
||||
{ $match: { orderDate: { $gte: startDate } } },
|
||||
{ $group: {
|
||||
_id: {
|
||||
year: { $year: "$orderDate" },
|
||||
month: { $month: "$orderDate" },
|
||||
day: { $dayOfMonth: "$orderDate" }
|
||||
},
|
||||
revenue: { $sum: "$total" },
|
||||
orderCount: { $sum: 1 }
|
||||
}},
|
||||
{ $sort: { "_id.year": 1, "_id.month": 1, "_id.day": 1 } }
|
||||
])
|
||||
```
|
||||
|
||||
### Faceted Search
|
||||
```javascript
|
||||
// Multiple aggregations in one query
|
||||
db.products.aggregate([
|
||||
{ $match: { category: "electronics" } },
|
||||
{ $facet: {
|
||||
priceRanges: [
|
||||
{ $bucket: {
|
||||
groupBy: "$price",
|
||||
boundaries: [0, 100, 500, 1000, 5000],
|
||||
default: "5000+",
|
||||
output: { count: { $sum: 1 } }
|
||||
}}
|
||||
],
|
||||
topBrands: [
|
||||
{ $group: { _id: "$brand", count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 5 }
|
||||
],
|
||||
avgPrice: [
|
||||
{ $group: { _id: null, avg: { $avg: "$price" } } }
|
||||
]
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### Window Functions
|
||||
```javascript
|
||||
// Running totals and moving averages
|
||||
db.sales.aggregate([
|
||||
{ $setWindowFields: {
|
||||
partitionBy: "$region",
|
||||
sortBy: { date: 1 },
|
||||
output: {
|
||||
runningTotal: {
|
||||
$sum: "$amount",
|
||||
window: { documents: ["unbounded", "current"] }
|
||||
},
|
||||
movingAvg: {
|
||||
$avg: "$amount",
|
||||
window: { documents: [-7, 0] } // Last 7 days
|
||||
}
|
||||
}
|
||||
}}
|
||||
])
|
||||
```
|
||||
|
||||
### Text Search with Aggregation
|
||||
```javascript
|
||||
// Full-text search (requires text index)
|
||||
db.articles.aggregate([
|
||||
{ $match: { $text: { $search: "mongodb database" } } },
|
||||
{ $addFields: { score: { $meta: "textScore" } } },
|
||||
{ $sort: { score: -1 } },
|
||||
{ $limit: 10 }
|
||||
])
|
||||
```
|
||||
|
||||
### Geospatial Aggregation
|
||||
```javascript
|
||||
// Find nearby locations
|
||||
db.places.aggregate([
|
||||
{ $geoNear: {
|
||||
near: { type: "Point", coordinates: [lon, lat] },
|
||||
distanceField: "distance",
|
||||
maxDistance: 5000,
|
||||
spherical: true
|
||||
}},
|
||||
{ $limit: 10 }
|
||||
])
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **$match early** - Filter documents before other stages
|
||||
2. **$project early** - Reduce document size
|
||||
3. **Index usage** - $match and $sort can use indexes (only at start)
|
||||
4. **$limit after $sort** - Reduce memory usage
|
||||
5. **Avoid $lookup** - Prefer embedded documents when possible
|
||||
6. **Use $facet sparingly** - Can be memory intensive
|
||||
7. **allowDiskUse** - Enable for large datasets
|
||||
```javascript
|
||||
db.collection.aggregate(pipeline, { allowDiskUse: true })
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Order stages efficiently** - $match → $project → $group → $sort → $limit
|
||||
2. **Use $expr carefully** - Can prevent index usage
|
||||
3. **Monitor memory** - Default limit: 100MB per stage
|
||||
4. **Test with explain** - Analyze pipeline performance
|
||||
```javascript
|
||||
db.collection.explain("executionStats").aggregate(pipeline)
|
||||
```
|
||||
5. **Break complex pipelines** - Use $out/$merge for intermediate results
|
||||
6. **Use $sample** - For random document selection
|
||||
7. **Leverage $addFields** - Cleaner than $project for adding fields
|
||||
465
skills/databases/references/mongodb-atlas.md
Normal file
465
skills/databases/references/mongodb-atlas.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# MongoDB Atlas Cloud Platform
|
||||
|
||||
MongoDB Atlas is fully-managed cloud database service with automated backups, monitoring, and scaling.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Create Free Cluster
|
||||
1. Sign up at mongodb.com/atlas
|
||||
2. Create organization and project
|
||||
3. Build cluster (M0 Free Tier)
|
||||
- Cloud provider: AWS/GCP/Azure
|
||||
- Region: closest to users
|
||||
- Cluster name
|
||||
4. Create database user (username/password)
|
||||
5. Whitelist IP address (or 0.0.0.0/0 for development)
|
||||
6. Get connection string
|
||||
|
||||
### Connection String Format
|
||||
```
|
||||
mongodb+srv://username:password@cluster.mongodb.net/database?retryWrites=true&w=majority
|
||||
```
|
||||
|
||||
### Connect
|
||||
```javascript
|
||||
// Node.js
|
||||
const { MongoClient } = require("mongodb");
|
||||
const uri = "mongodb+srv://...";
|
||||
const client = new MongoClient(uri);
|
||||
|
||||
await client.connect();
|
||||
const db = client.db("myDatabase");
|
||||
```
|
||||
|
||||
```python
|
||||
# Python
|
||||
from pymongo import MongoClient
|
||||
uri = "mongodb+srv://..."
|
||||
client = MongoClient(uri)
|
||||
db = client.myDatabase
|
||||
```
|
||||
|
||||
## Cluster Tiers
|
||||
|
||||
### M0 (Free Tier)
|
||||
- 512 MB storage
|
||||
- Shared CPU/RAM
|
||||
- Perfect for development/learning
|
||||
- Limited to 100 connections
|
||||
- No backups
|
||||
|
||||
### M10+ (Dedicated Clusters)
|
||||
- Dedicated resources
|
||||
- 2GB - 4TB+ storage
|
||||
- Automated backups
|
||||
- Advanced monitoring
|
||||
- Performance Advisor
|
||||
- Multi-region support
|
||||
- VPC peering
|
||||
|
||||
### Serverless
|
||||
- Pay per operation
|
||||
- Auto-scales to zero
|
||||
- Good for sporadic workloads
|
||||
- 1GB+ storage
|
||||
- Limited features (no full-text search)
|
||||
|
||||
## Database Configuration
|
||||
|
||||
### Create Database
|
||||
```javascript
|
||||
// Via Atlas UI: Database → Add Database
|
||||
// Via shell
|
||||
use myNewDatabase
|
||||
db.createCollection("myCollection")
|
||||
|
||||
// Via driver
|
||||
const db = client.db("myNewDatabase");
|
||||
await db.createCollection("myCollection");
|
||||
```
|
||||
|
||||
### Schema Validation
|
||||
```javascript
|
||||
// Set validation rules in Atlas UI or via shell
|
||||
db.createCollection("users", {
|
||||
validator: {
|
||||
$jsonSchema: {
|
||||
bsonType: "object",
|
||||
required: ["email", "name"],
|
||||
properties: {
|
||||
email: { bsonType: "string", pattern: "^.+@.+$" },
|
||||
age: { bsonType: "int", minimum: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Network Access
|
||||
```javascript
|
||||
// IP Whitelist (Atlas UI → Network Access)
|
||||
// - Add IP Address: specific IPs
|
||||
// - 0.0.0.0/0: allow from anywhere (dev only)
|
||||
// - VPC Peering: private connection
|
||||
|
||||
// Connection string includes options
|
||||
mongodb+srv://cluster.mongodb.net/?retryWrites=true&w=majority&ssl=true
|
||||
```
|
||||
|
||||
### Database Users
|
||||
```javascript
|
||||
// Create via Atlas UI → Database Access
|
||||
// - Username/password authentication
|
||||
// - AWS IAM authentication
|
||||
// - X.509 certificates
|
||||
|
||||
// Roles:
|
||||
// - atlasAdmin: full access
|
||||
// - readWriteAnyDatabase: read/write all databases
|
||||
// - readAnyDatabase: read-only all databases
|
||||
// - read/readWrite: database-specific
|
||||
```
|
||||
|
||||
### Encryption
|
||||
```javascript
|
||||
// Encryption at rest (automatic on M10+)
|
||||
// Encryption in transit (TLS/SSL, always enabled)
|
||||
|
||||
// Client-Side Field Level Encryption (CSFLE)
|
||||
const autoEncryptionOpts = {
|
||||
keyVaultNamespace: "encryption.__keyVault",
|
||||
kmsProviders: {
|
||||
aws: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const client = new MongoClient(uri, { autoEncryption: autoEncryptionOpts });
|
||||
```
|
||||
|
||||
## Backups and Snapshots
|
||||
|
||||
### Cloud Backups (M10+)
|
||||
```javascript
|
||||
// Automatic continuous backups
|
||||
// - Snapshots every 6-24 hours
|
||||
// - Oplog for point-in-time recovery
|
||||
// - Retention: 2+ days configurable
|
||||
|
||||
// Restore via Atlas UI:
|
||||
// 1. Clusters → cluster name → Backup tab
|
||||
// 2. Select snapshot or point in time
|
||||
// 3. Download or restore to cluster
|
||||
```
|
||||
|
||||
### Manual Backups
|
||||
```bash
|
||||
# Export using mongodump
|
||||
mongodump --uri="mongodb+srv://user:pass@cluster.mongodb.net/mydb" --out=/backup
|
||||
|
||||
# Restore using mongorestore
|
||||
mongorestore --uri="mongodb+srv://..." /backup/mydb
|
||||
```
|
||||
|
||||
## Monitoring and Alerts
|
||||
|
||||
### Metrics Dashboard
|
||||
```javascript
|
||||
// Atlas UI → Metrics
|
||||
// Key metrics:
|
||||
// - Operations per second
|
||||
// - Query execution times
|
||||
// - Connections
|
||||
// - Network I/O
|
||||
// - Disk usage
|
||||
// - CPU utilization
|
||||
|
||||
// Real-time Performance panel
|
||||
// - Current operations
|
||||
// - Slow queries
|
||||
// - Index suggestions
|
||||
```
|
||||
|
||||
### Alerts
|
||||
```javascript
|
||||
// Configure via Atlas UI → Alerts
|
||||
// Alert types:
|
||||
// - High connections (> threshold)
|
||||
// - High CPU usage (> 80%)
|
||||
// - Disk usage (> 90%)
|
||||
// - Replication lag
|
||||
// - Backup failures
|
||||
|
||||
// Notification channels:
|
||||
// - Email
|
||||
// - SMS
|
||||
// - Slack
|
||||
// - PagerDuty
|
||||
// - Webhook
|
||||
```
|
||||
|
||||
### Performance Advisor
|
||||
```javascript
|
||||
// Automatic index recommendations
|
||||
// Atlas UI → Performance Advisor
|
||||
|
||||
// Analyzes:
|
||||
// - Slow queries
|
||||
// - Missing indexes
|
||||
// - Redundant indexes
|
||||
// - Index usage statistics
|
||||
|
||||
// Provides:
|
||||
// - Index creation commands
|
||||
// - Expected performance improvement
|
||||
// - Schema design suggestions
|
||||
```
|
||||
|
||||
## Atlas Search (Full-Text Search)
|
||||
|
||||
### Create Search Index
|
||||
```javascript
|
||||
// Atlas UI → Search → Create Index
|
||||
|
||||
// JSON definition
|
||||
{
|
||||
"mappings": {
|
||||
"dynamic": false,
|
||||
"fields": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"analyzer": "lucene.standard"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"analyzer": "lucene.english"
|
||||
},
|
||||
"tags": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Search Queries
|
||||
```javascript
|
||||
// Aggregation pipeline with $search
|
||||
db.articles.aggregate([
|
||||
{
|
||||
$search: {
|
||||
text: {
|
||||
query: "mongodb database tutorial",
|
||||
path: ["title", "description"],
|
||||
fuzzy: { maxEdits: 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $limit: 10 },
|
||||
{
|
||||
$project: {
|
||||
title: 1,
|
||||
description: 1,
|
||||
score: { $meta: "searchScore" }
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Autocomplete
|
||||
db.articles.aggregate([
|
||||
{
|
||||
$search: {
|
||||
autocomplete: {
|
||||
query: "mong",
|
||||
path: "title",
|
||||
tokenOrder: "sequential"
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
## Atlas Vector Search (AI/ML)
|
||||
|
||||
### Create Vector Search Index
|
||||
```javascript
|
||||
// For AI similarity search (embeddings)
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"type": "vector",
|
||||
"path": "embedding",
|
||||
"numDimensions": 1536, // OpenAI embeddings
|
||||
"similarity": "cosine"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Vector Search Query
|
||||
```javascript
|
||||
// Search by similarity
|
||||
db.products.aggregate([
|
||||
{
|
||||
$vectorSearch: {
|
||||
index: "vector_index",
|
||||
path: "embedding",
|
||||
queryVector: [0.123, 0.456, ...], // 1536 dimensions
|
||||
numCandidates: 100,
|
||||
limit: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
name: 1,
|
||||
description: 1,
|
||||
score: { $meta: "vectorSearchScore" }
|
||||
}
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
## Data Federation
|
||||
|
||||
### Query Across Sources
|
||||
```javascript
|
||||
// Federated database instance
|
||||
// Query data from:
|
||||
// - Atlas clusters
|
||||
// - AWS S3
|
||||
// - HTTP endpoints
|
||||
|
||||
// Create virtual collection
|
||||
{
|
||||
"databases": [{
|
||||
"name": "federated",
|
||||
"collections": [{
|
||||
"name": "sales",
|
||||
"dataSources": [{
|
||||
"storeName": "s3Store",
|
||||
"path": "/sales/*.json"
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
// Query like normal collection
|
||||
use federated
|
||||
db.sales.find({ region: "US" })
|
||||
```
|
||||
|
||||
## Atlas Charts (Embedded Analytics)
|
||||
|
||||
### Create Dashboard
|
||||
```javascript
|
||||
// Atlas UI → Charts → New Dashboard
|
||||
// Data source: Atlas cluster
|
||||
// Chart types: bar, line, pie, scatter, etc.
|
||||
|
||||
// Embed in application
|
||||
<iframe
|
||||
src="https://charts.mongodb.com/charts-project/embed/charts?id=..."
|
||||
width="800"
|
||||
height="600"
|
||||
/>
|
||||
```
|
||||
|
||||
## Atlas CLI
|
||||
|
||||
```bash
|
||||
# Install
|
||||
npm install -g mongodb-atlas-cli
|
||||
|
||||
# Login
|
||||
atlas auth login
|
||||
|
||||
# List clusters
|
||||
atlas clusters list
|
||||
|
||||
# Create cluster
|
||||
atlas clusters create myCluster --provider AWS --region US_EAST_1 --tier M10
|
||||
|
||||
# Manage users
|
||||
atlas dbusers create --username myuser --password mypass
|
||||
|
||||
# Backups
|
||||
atlas backups snapshots list --clusterName myCluster
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use connection pooling** - Reuse connections
|
||||
```javascript
|
||||
const client = new MongoClient(uri, {
|
||||
maxPoolSize: 50,
|
||||
minPoolSize: 10
|
||||
});
|
||||
```
|
||||
|
||||
2. **Enable authentication** - Always use database users, not Atlas users
|
||||
|
||||
3. **Restrict network access** - IP whitelist or VPC peering
|
||||
|
||||
4. **Monitor regularly** - Set up alerts for key metrics
|
||||
|
||||
5. **Index optimization** - Use Performance Advisor recommendations
|
||||
|
||||
6. **Backup verification** - Regularly test restores
|
||||
|
||||
7. **Right-size clusters** - Start small, scale as needed
|
||||
|
||||
8. **Multi-region** - For global applications (M10+)
|
||||
|
||||
9. **Read preferences** - Use secondaries for read-heavy workloads
|
||||
```javascript
|
||||
const client = new MongoClient(uri, {
|
||||
readPreference: "secondaryPreferred"
|
||||
});
|
||||
```
|
||||
|
||||
10. **Connection string security** - Use environment variables
|
||||
```javascript
|
||||
const uri = process.env.MONGODB_URI;
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
```javascript
|
||||
// Check IP whitelist
|
||||
// Verify credentials
|
||||
// Test connection string
|
||||
|
||||
// Verbose logging
|
||||
const client = new MongoClient(uri, {
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
loggerLevel: "debug"
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
```javascript
|
||||
// Check Performance Advisor
|
||||
// Review slow query logs
|
||||
// Analyze index usage
|
||||
db.collection.aggregate([{ $indexStats: {} }])
|
||||
|
||||
// Check connection count
|
||||
db.serverStatus().connections
|
||||
```
|
||||
|
||||
### Common Errors
|
||||
```javascript
|
||||
// MongoNetworkError: IP not whitelisted
|
||||
// → Add IP to Network Access
|
||||
|
||||
// Authentication failed: wrong credentials
|
||||
// → Verify username/password in Database Access
|
||||
|
||||
// Timeout: connection string or network issue
|
||||
// → Check connection string format, DNS resolution
|
||||
```
|
||||
408
skills/databases/references/mongodb-crud.md
Normal file
408
skills/databases/references/mongodb-crud.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# MongoDB CRUD Operations
|
||||
|
||||
CRUD operations (Create, Read, Update, Delete) in MongoDB with query operators and atomic updates.
|
||||
|
||||
## Create Operations
|
||||
|
||||
### insertOne
|
||||
```javascript
|
||||
// Insert single document
|
||||
db.users.insertOne({
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
age: 30,
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
// Returns: { acknowledged: true, insertedId: ObjectId("...") }
|
||||
```
|
||||
|
||||
### insertMany
|
||||
```javascript
|
||||
// Insert multiple documents
|
||||
db.users.insertMany([
|
||||
{ name: "Bob", age: 25 },
|
||||
{ name: "Charlie", age: 35 },
|
||||
{ name: "Diana", age: 28 }
|
||||
])
|
||||
|
||||
// With ordered: false (continue on error)
|
||||
db.users.insertMany(docs, { ordered: false })
|
||||
```
|
||||
|
||||
## Read Operations
|
||||
|
||||
### find
|
||||
```javascript
|
||||
// Find all documents
|
||||
db.users.find()
|
||||
|
||||
// Find with filter
|
||||
db.users.find({ age: { $gte: 18 } })
|
||||
|
||||
// Projection (select fields)
|
||||
db.users.find({ status: "active" }, { name: 1, email: 1, _id: 0 })
|
||||
|
||||
// Cursor operations
|
||||
db.users.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10)
|
||||
.skip(20)
|
||||
```
|
||||
|
||||
### findOne
|
||||
```javascript
|
||||
// Get single document
|
||||
db.users.findOne({ email: "alice@example.com" })
|
||||
|
||||
// With projection
|
||||
db.users.findOne({ _id: ObjectId("...") }, { name: 1, email: 1 })
|
||||
```
|
||||
|
||||
### count/estimatedDocumentCount
|
||||
```javascript
|
||||
// Count matching documents
|
||||
db.users.countDocuments({ status: "active" })
|
||||
|
||||
// Fast estimate (uses metadata)
|
||||
db.users.estimatedDocumentCount()
|
||||
```
|
||||
|
||||
### distinct
|
||||
```javascript
|
||||
// Get unique values
|
||||
db.users.distinct("status")
|
||||
db.users.distinct("city", { country: "USA" })
|
||||
```
|
||||
|
||||
## Update Operations
|
||||
|
||||
### updateOne
|
||||
```javascript
|
||||
// Update first matching document
|
||||
db.users.updateOne(
|
||||
{ email: "alice@example.com" },
|
||||
{ $set: { status: "verified" } }
|
||||
)
|
||||
|
||||
// Upsert (insert if not exists)
|
||||
db.users.updateOne(
|
||||
{ email: "new@example.com" },
|
||||
{ $set: { name: "New User" } },
|
||||
{ upsert: true }
|
||||
)
|
||||
```
|
||||
|
||||
### updateMany
|
||||
```javascript
|
||||
// Update all matching documents
|
||||
db.users.updateMany(
|
||||
{ lastLogin: { $lt: cutoffDate } },
|
||||
{ $set: { status: "inactive" } }
|
||||
)
|
||||
|
||||
// Multiple updates
|
||||
db.users.updateMany(
|
||||
{ status: "pending" },
|
||||
{
|
||||
$set: { status: "active" },
|
||||
$currentDate: { updatedAt: true }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### replaceOne
|
||||
```javascript
|
||||
// Replace entire document (except _id)
|
||||
db.users.replaceOne(
|
||||
{ _id: ObjectId("...") },
|
||||
{ name: "Alice", email: "alice@example.com", age: 31 }
|
||||
)
|
||||
```
|
||||
|
||||
## Delete Operations
|
||||
|
||||
### deleteOne
|
||||
```javascript
|
||||
// Delete first matching document
|
||||
db.users.deleteOne({ email: "alice@example.com" })
|
||||
```
|
||||
|
||||
### deleteMany
|
||||
```javascript
|
||||
// Delete all matching documents
|
||||
db.users.deleteMany({ status: "deleted" })
|
||||
|
||||
// Delete all documents in collection
|
||||
db.users.deleteMany({})
|
||||
```
|
||||
|
||||
## Query Operators
|
||||
|
||||
### Comparison Operators
|
||||
```javascript
|
||||
// $eq (equals)
|
||||
db.users.find({ age: { $eq: 30 } })
|
||||
db.users.find({ age: 30 }) // Implicit $eq
|
||||
|
||||
// $ne (not equals)
|
||||
db.users.find({ status: { $ne: "deleted" } })
|
||||
|
||||
// $gt, $gte, $lt, $lte
|
||||
db.users.find({ age: { $gt: 18, $lte: 65 } })
|
||||
|
||||
// $in (in array)
|
||||
db.users.find({ status: { $in: ["active", "pending"] } })
|
||||
|
||||
// $nin (not in array)
|
||||
db.users.find({ status: { $nin: ["deleted", "banned"] } })
|
||||
```
|
||||
|
||||
### Logical Operators
|
||||
```javascript
|
||||
// $and (implicit for multiple conditions)
|
||||
db.users.find({ age: { $gte: 18 }, status: "active" })
|
||||
|
||||
// $and (explicit)
|
||||
db.users.find({
|
||||
$and: [
|
||||
{ age: { $gte: 18 } },
|
||||
{ status: "active" }
|
||||
]
|
||||
})
|
||||
|
||||
// $or
|
||||
db.users.find({
|
||||
$or: [
|
||||
{ status: "active" },
|
||||
{ verified: true }
|
||||
]
|
||||
})
|
||||
|
||||
// $not
|
||||
db.users.find({ age: { $not: { $lt: 18 } } })
|
||||
|
||||
// $nor (not any condition)
|
||||
db.users.find({
|
||||
$nor: [
|
||||
{ status: "deleted" },
|
||||
{ status: "banned" }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Element Operators
|
||||
```javascript
|
||||
// $exists
|
||||
db.users.find({ phoneNumber: { $exists: true } })
|
||||
db.users.find({ deletedAt: { $exists: false } })
|
||||
|
||||
// $type
|
||||
db.users.find({ age: { $type: "int" } })
|
||||
db.users.find({ age: { $type: ["int", "double"] } })
|
||||
```
|
||||
|
||||
### Array Operators
|
||||
```javascript
|
||||
// $all (contains all elements)
|
||||
db.posts.find({ tags: { $all: ["mongodb", "database"] } })
|
||||
|
||||
// $elemMatch (array element matches all conditions)
|
||||
db.products.find({
|
||||
reviews: {
|
||||
$elemMatch: { rating: { $gte: 4 }, verified: true }
|
||||
}
|
||||
})
|
||||
|
||||
// $size (array length)
|
||||
db.posts.find({ tags: { $size: 3 } })
|
||||
```
|
||||
|
||||
### String Operators
|
||||
```javascript
|
||||
// $regex (regular expression)
|
||||
db.users.find({ name: { $regex: /^A/i } })
|
||||
db.users.find({ email: { $regex: "@example\\.com$" } })
|
||||
|
||||
// Text search (requires text index)
|
||||
db.articles.find({ $text: { $search: "mongodb database" } })
|
||||
```
|
||||
|
||||
## Update Operators
|
||||
|
||||
### Field Update Operators
|
||||
```javascript
|
||||
// $set (set field value)
|
||||
db.users.updateOne(
|
||||
{ _id: userId },
|
||||
{ $set: { status: "active", updatedAt: new Date() } }
|
||||
)
|
||||
|
||||
// $unset (remove field)
|
||||
db.users.updateOne(
|
||||
{ _id: userId },
|
||||
{ $unset: { tempField: "" } }
|
||||
)
|
||||
|
||||
// $rename (rename field)
|
||||
db.users.updateMany(
|
||||
{},
|
||||
{ $rename: { "oldName": "newName" } }
|
||||
)
|
||||
|
||||
// $currentDate (set to current date)
|
||||
db.users.updateOne(
|
||||
{ _id: userId },
|
||||
{ $currentDate: { lastModified: true } }
|
||||
)
|
||||
```
|
||||
|
||||
### Numeric Update Operators
|
||||
```javascript
|
||||
// $inc (increment)
|
||||
db.posts.updateOne(
|
||||
{ _id: postId },
|
||||
{ $inc: { views: 1, likes: 5 } }
|
||||
)
|
||||
|
||||
// $mul (multiply)
|
||||
db.products.updateOne(
|
||||
{ _id: productId },
|
||||
{ $mul: { price: 1.1 } } // 10% increase
|
||||
)
|
||||
|
||||
// $min (update if new value is less)
|
||||
db.scores.updateOne(
|
||||
{ _id: scoreId },
|
||||
{ $min: { lowestScore: 50 } }
|
||||
)
|
||||
|
||||
// $max (update if new value is greater)
|
||||
db.scores.updateOne(
|
||||
{ _id: scoreId },
|
||||
{ $max: { highestScore: 100 } }
|
||||
)
|
||||
```
|
||||
|
||||
### Array Update Operators
|
||||
```javascript
|
||||
// $push (add to array)
|
||||
db.posts.updateOne(
|
||||
{ _id: postId },
|
||||
{ $push: { comments: { author: "Alice", text: "Great!" } } }
|
||||
)
|
||||
|
||||
// $push with $each (multiple elements)
|
||||
db.posts.updateOne(
|
||||
{ _id: postId },
|
||||
{ $push: { tags: { $each: ["mongodb", "database"] } } }
|
||||
)
|
||||
|
||||
// $addToSet (add if not exists)
|
||||
db.users.updateOne(
|
||||
{ _id: userId },
|
||||
{ $addToSet: { interests: "coding" } }
|
||||
)
|
||||
|
||||
// $pull (remove matching elements)
|
||||
db.users.updateOne(
|
||||
{ _id: userId },
|
||||
{ $pull: { tags: "deprecated" } }
|
||||
)
|
||||
|
||||
// $pop (remove first/last element)
|
||||
db.users.updateOne(
|
||||
{ _id: userId },
|
||||
{ $pop: { notifications: -1 } } // -1: first, 1: last
|
||||
)
|
||||
|
||||
// $ (update first matching array element)
|
||||
db.posts.updateOne(
|
||||
{ _id: postId, "comments.author": "Alice" },
|
||||
{ $set: { "comments.$.text": "Updated comment" } }
|
||||
)
|
||||
|
||||
// $[] (update all array elements)
|
||||
db.posts.updateOne(
|
||||
{ _id: postId },
|
||||
{ $set: { "comments.$[].verified": true } }
|
||||
)
|
||||
|
||||
// $[<identifier>] (filtered positional)
|
||||
db.posts.updateOne(
|
||||
{ _id: postId },
|
||||
{ $set: { "comments.$[elem].flagged": true } },
|
||||
{ arrayFilters: [{ "elem.rating": { $lt: 2 } }] }
|
||||
)
|
||||
```
|
||||
|
||||
## Atomic Operations
|
||||
|
||||
### findAndModify / findOneAndUpdate
|
||||
```javascript
|
||||
// Find and update (returns old document by default)
|
||||
db.users.findOneAndUpdate(
|
||||
{ email: "alice@example.com" },
|
||||
{ $set: { status: "active" } }
|
||||
)
|
||||
|
||||
// Return new document
|
||||
db.users.findOneAndUpdate(
|
||||
{ email: "alice@example.com" },
|
||||
{ $set: { status: "active" } },
|
||||
{ returnNewDocument: true }
|
||||
)
|
||||
|
||||
// Upsert and return new
|
||||
db.counters.findOneAndUpdate(
|
||||
{ _id: "sequence" },
|
||||
{ $inc: { value: 1 } },
|
||||
{ upsert: true, returnNewDocument: true }
|
||||
)
|
||||
```
|
||||
|
||||
### findOneAndReplace
|
||||
```javascript
|
||||
// Find and replace entire document
|
||||
db.users.findOneAndReplace(
|
||||
{ _id: ObjectId("...") },
|
||||
{ name: "Alice", email: "alice@example.com" },
|
||||
{ returnNewDocument: true }
|
||||
)
|
||||
```
|
||||
|
||||
### findOneAndDelete
|
||||
```javascript
|
||||
// Find and delete (returns deleted document)
|
||||
const deletedUser = db.users.findOneAndDelete(
|
||||
{ email: "alice@example.com" }
|
||||
)
|
||||
```
|
||||
|
||||
## Bulk Operations
|
||||
|
||||
```javascript
|
||||
// Ordered bulk write (stops on first error)
|
||||
db.users.bulkWrite([
|
||||
{ insertOne: { document: { name: "Alice" } } },
|
||||
{ updateOne: {
|
||||
filter: { name: "Bob" },
|
||||
update: { $set: { age: 25 } }
|
||||
}},
|
||||
{ deleteOne: { filter: { name: "Charlie" } } }
|
||||
])
|
||||
|
||||
// Unordered (continues on errors)
|
||||
db.users.bulkWrite(operations, { ordered: false })
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use projection** to return only needed fields
|
||||
2. **Create indexes** on frequently queried fields
|
||||
3. **Use updateMany** carefully (can affect many documents)
|
||||
4. **Use upsert** for "create or update" patterns
|
||||
5. **Use atomic operators** ($inc, $push) for concurrent updates
|
||||
6. **Avoid large arrays** in documents (embed vs reference)
|
||||
7. **Use findAndModify** for atomic read-modify-write
|
||||
8. **Batch operations** with insertMany/bulkWrite for efficiency
|
||||
442
skills/databases/references/mongodb-indexing.md
Normal file
442
skills/databases/references/mongodb-indexing.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# MongoDB Indexing and Performance
|
||||
|
||||
Index types, strategies, and performance optimization techniques for MongoDB.
|
||||
|
||||
## Index Fundamentals
|
||||
|
||||
Indexes improve query performance by allowing MongoDB to scan fewer documents. Without indexes, MongoDB performs collection scans (reads every document).
|
||||
|
||||
```javascript
|
||||
// Check if query uses index
|
||||
db.users.find({ email: "user@example.com" }).explain("executionStats")
|
||||
|
||||
// Key metrics:
|
||||
// - executionTimeMillis: query duration
|
||||
// - totalDocsExamined: documents scanned
|
||||
// - nReturned: documents returned
|
||||
// - stage: IXSCAN (index) vs COLLSCAN (full scan)
|
||||
```
|
||||
|
||||
## Index Types
|
||||
|
||||
### Single Field Index
|
||||
```javascript
|
||||
// Create index on single field
|
||||
db.users.createIndex({ email: 1 }) // 1: ascending, -1: descending
|
||||
|
||||
// Use case: queries filtering by email
|
||||
db.users.find({ email: "user@example.com" })
|
||||
|
||||
// Drop index
|
||||
db.users.dropIndex({ email: 1 })
|
||||
db.users.dropIndex("email_1") // By name
|
||||
```
|
||||
|
||||
### Compound Index
|
||||
```javascript
|
||||
// Index on multiple fields (order matters!)
|
||||
db.orders.createIndex({ status: 1, createdAt: -1 })
|
||||
|
||||
// Supports queries on:
|
||||
// 1. { status: "..." }
|
||||
// 2. { status: "...", createdAt: ... }
|
||||
// Does NOT efficiently support: { createdAt: ... } alone
|
||||
|
||||
// Left-to-right prefix rule
|
||||
db.orders.createIndex({ a: 1, b: 1, c: 1 })
|
||||
// Supports: {a}, {a,b}, {a,b,c}
|
||||
// Not: {b}, {c}, {b,c}
|
||||
```
|
||||
|
||||
### Text Index (Full-Text Search)
|
||||
```javascript
|
||||
// Create text index
|
||||
db.articles.createIndex({ title: "text", body: "text" })
|
||||
|
||||
// Only one text index per collection
|
||||
db.articles.createIndex({
|
||||
title: "text",
|
||||
body: "text",
|
||||
tags: "text"
|
||||
}, {
|
||||
weights: {
|
||||
title: 10, // Title matches weighted higher
|
||||
body: 5,
|
||||
tags: 3
|
||||
}
|
||||
})
|
||||
|
||||
// Search
|
||||
db.articles.find({ $text: { $search: "mongodb database" } })
|
||||
|
||||
// Search with score
|
||||
db.articles.find(
|
||||
{ $text: { $search: "mongodb" } },
|
||||
{ score: { $meta: "textScore" } }
|
||||
).sort({ score: { $meta: "textScore" } })
|
||||
```
|
||||
|
||||
### Geospatial Indexes
|
||||
```javascript
|
||||
// 2dsphere index (spherical geometry)
|
||||
db.places.createIndex({ location: "2dsphere" })
|
||||
|
||||
// Document format
|
||||
db.places.insertOne({
|
||||
name: "Coffee Shop",
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [-73.97, 40.77] // [longitude, latitude]
|
||||
}
|
||||
})
|
||||
|
||||
// Find nearby
|
||||
db.places.find({
|
||||
location: {
|
||||
$near: {
|
||||
$geometry: { type: "Point", coordinates: [-73.97, 40.77] },
|
||||
$maxDistance: 5000 // meters
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Within polygon
|
||||
db.places.find({
|
||||
location: {
|
||||
$geoWithin: {
|
||||
$geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[lon1, lat1], [lon2, lat2], [lon3, lat3], [lon1, lat1]
|
||||
]]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Wildcard Index
|
||||
```javascript
|
||||
// Index all fields in subdocuments
|
||||
db.products.createIndex({ "attributes.$**": 1 })
|
||||
|
||||
// Supports queries on any nested field
|
||||
db.products.find({ "attributes.color": "red" })
|
||||
db.products.find({ "attributes.size": "large" })
|
||||
|
||||
// Specific paths only
|
||||
db.products.createIndex(
|
||||
{ "$**": 1 },
|
||||
{ wildcardProjection: { "attributes.color": 1, "attributes.size": 1 } }
|
||||
)
|
||||
```
|
||||
|
||||
### Hashed Index
|
||||
```javascript
|
||||
// Hashed index (for even distribution in sharding)
|
||||
db.users.createIndex({ userId: "hashed" })
|
||||
|
||||
// Use case: shard key
|
||||
sh.shardCollection("mydb.users", { userId: "hashed" })
|
||||
```
|
||||
|
||||
### TTL Index (Auto-Expiration)
|
||||
```javascript
|
||||
// Delete documents after specified time
|
||||
db.sessions.createIndex(
|
||||
{ createdAt: 1 },
|
||||
{ expireAfterSeconds: 3600 } // 1 hour
|
||||
)
|
||||
|
||||
// Documents automatically deleted after createdAt + 3600 seconds
|
||||
// Background task runs every 60 seconds
|
||||
```
|
||||
|
||||
### Partial Index
|
||||
```javascript
|
||||
// Index only documents matching filter
|
||||
db.orders.createIndex(
|
||||
{ customerId: 1 },
|
||||
{ partialFilterExpression: { status: "active" } }
|
||||
)
|
||||
|
||||
// Index only used when query includes filter
|
||||
db.orders.find({ customerId: "123", status: "active" }) // Uses index
|
||||
db.orders.find({ customerId: "123" }) // Does not use index
|
||||
```
|
||||
|
||||
### Unique Index
|
||||
```javascript
|
||||
// Enforce uniqueness
|
||||
db.users.createIndex({ email: 1 }, { unique: true })
|
||||
|
||||
// Compound unique index
|
||||
db.users.createIndex({ firstName: 1, lastName: 1 }, { unique: true })
|
||||
|
||||
// Sparse unique index (null values not indexed)
|
||||
db.users.createIndex({ email: 1 }, { unique: true, sparse: true })
|
||||
```
|
||||
|
||||
### Sparse Index
|
||||
```javascript
|
||||
// Index only documents with field present
|
||||
db.users.createIndex({ phoneNumber: 1 }, { sparse: true })
|
||||
|
||||
// Useful for optional fields
|
||||
// Documents without phoneNumber not in index
|
||||
```
|
||||
|
||||
## Index Management
|
||||
|
||||
### List Indexes
|
||||
```javascript
|
||||
// Show all indexes
|
||||
db.collection.getIndexes()
|
||||
|
||||
// Index statistics
|
||||
db.collection.aggregate([{ $indexStats: {} }])
|
||||
```
|
||||
|
||||
### Create Index Options
|
||||
```javascript
|
||||
// Background index (doesn't block operations)
|
||||
db.collection.createIndex({ field: 1 }, { background: true })
|
||||
|
||||
// Index name
|
||||
db.collection.createIndex({ field: 1 }, { name: "custom_index_name" })
|
||||
|
||||
// Case-insensitive index (collation)
|
||||
db.collection.createIndex(
|
||||
{ name: 1 },
|
||||
{ collation: { locale: "en", strength: 2 } }
|
||||
)
|
||||
```
|
||||
|
||||
### Hide/Unhide Index
|
||||
```javascript
|
||||
// Hide index (test before dropping)
|
||||
db.collection.hideIndex("index_name")
|
||||
|
||||
// Check performance without index
|
||||
// ...
|
||||
|
||||
// Unhide or drop
|
||||
db.collection.unhideIndex("index_name")
|
||||
db.collection.dropIndex("index_name")
|
||||
```
|
||||
|
||||
### Rebuild Indexes
|
||||
```javascript
|
||||
// Rebuild all indexes (after data changes)
|
||||
db.collection.reIndex()
|
||||
|
||||
// Useful after bulk deletions to reclaim space
|
||||
```
|
||||
|
||||
## Query Optimization
|
||||
|
||||
### Covered Queries
|
||||
```javascript
|
||||
// Query covered by index (no document fetch)
|
||||
db.users.createIndex({ email: 1, name: 1 })
|
||||
|
||||
// Covered query (all fields in index)
|
||||
db.users.find(
|
||||
{ email: "user@example.com" },
|
||||
{ email: 1, name: 1, _id: 0 } // Must exclude _id
|
||||
)
|
||||
|
||||
// Check with explain: stage should be "IXSCAN" with no "FETCH"
|
||||
```
|
||||
|
||||
### Index Intersection
|
||||
```javascript
|
||||
// MongoDB can use multiple indexes
|
||||
db.collection.createIndex({ a: 1 })
|
||||
db.collection.createIndex({ b: 1 })
|
||||
|
||||
// Query may use both indexes
|
||||
db.collection.find({ a: 1, b: 1 })
|
||||
|
||||
// Usually compound index is better
|
||||
db.collection.createIndex({ a: 1, b: 1 })
|
||||
```
|
||||
|
||||
### Index Hints
|
||||
```javascript
|
||||
// Force specific index
|
||||
db.orders.find({ status: "active", city: "NYC" })
|
||||
.hint({ status: 1, createdAt: -1 })
|
||||
|
||||
// Force no index (for testing)
|
||||
db.orders.find({ status: "active" }).hint({ $natural: 1 })
|
||||
```
|
||||
|
||||
### ESR Rule (Equality, Sort, Range)
|
||||
```javascript
|
||||
// Optimal compound index order: Equality → Sort → Range
|
||||
|
||||
// Query
|
||||
db.orders.find({
|
||||
status: "completed", // Equality
|
||||
category: "electronics" // Equality
|
||||
}).sort({
|
||||
orderDate: -1 // Sort
|
||||
}).limit(10)
|
||||
|
||||
// Optimal index
|
||||
db.orders.createIndex({
|
||||
status: 1, // Equality first
|
||||
category: 1, // Equality
|
||||
orderDate: -1 // Sort last
|
||||
})
|
||||
|
||||
// With range
|
||||
db.orders.find({
|
||||
status: "completed", // Equality
|
||||
total: { $gte: 100 } // Range
|
||||
}).sort({
|
||||
orderDate: -1 // Sort
|
||||
})
|
||||
|
||||
// Optimal index
|
||||
db.orders.createIndex({
|
||||
status: 1, // Equality
|
||||
orderDate: -1, // Sort
|
||||
total: 1 // Range last
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Analysis
|
||||
|
||||
### explain() Modes
|
||||
```javascript
|
||||
// Query planner (default)
|
||||
db.collection.find({ field: value }).explain()
|
||||
|
||||
// Execution stats
|
||||
db.collection.find({ field: value }).explain("executionStats")
|
||||
|
||||
// All execution stats
|
||||
db.collection.find({ field: value }).explain("allPlansExecution")
|
||||
```
|
||||
|
||||
### Key Metrics
|
||||
```javascript
|
||||
// Good performance indicators:
|
||||
// - executionTimeMillis < 100ms
|
||||
// - totalDocsExamined ≈ nReturned (examine only what's needed)
|
||||
// - stage: "IXSCAN" (using index)
|
||||
// - totalKeysExamined ≈ nReturned (index selectivity)
|
||||
|
||||
// Bad indicators:
|
||||
// - stage: "COLLSCAN" (full collection scan)
|
||||
// - totalDocsExamined >> nReturned (scanning too many docs)
|
||||
// - executionTimeMillis > 1000ms
|
||||
```
|
||||
|
||||
### Index Selectivity
|
||||
```javascript
|
||||
// High selectivity = good (returns few documents)
|
||||
// Low selectivity = bad (returns many documents)
|
||||
|
||||
// Check selectivity
|
||||
db.collection.aggregate([
|
||||
{ $group: { _id: "$status", count: { $sum: 1 } } }
|
||||
])
|
||||
|
||||
// Good for indexing: email, userId, orderId
|
||||
// Bad for indexing: gender, status (few unique values)
|
||||
```
|
||||
|
||||
## Index Strategies
|
||||
|
||||
### Multi-Tenant Applications
|
||||
```javascript
|
||||
// Always filter by tenant first
|
||||
db.data.createIndex({ tenantId: 1, createdAt: -1 })
|
||||
|
||||
// All queries include tenantId
|
||||
db.data.find({ tenantId: "tenant1", createdAt: { $gte: date } })
|
||||
```
|
||||
|
||||
### Time-Series Data
|
||||
```javascript
|
||||
// Index on timestamp descending (recent data accessed more)
|
||||
db.events.createIndex({ timestamp: -1 })
|
||||
|
||||
// Compound with filter fields
|
||||
db.events.createIndex({ userId: 1, timestamp: -1 })
|
||||
```
|
||||
|
||||
### Lookup Optimization
|
||||
```javascript
|
||||
// Index foreign key fields
|
||||
db.orders.createIndex({ customerId: 1 })
|
||||
db.customers.createIndex({ _id: 1 }) // Default _id index
|
||||
|
||||
// Aggregation $lookup uses these indexes
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Create indexes for frequent queries** - Analyze slow query logs
|
||||
2. **Limit number of indexes** - Each index adds write overhead
|
||||
3. **Use compound indexes** - More efficient than multiple single indexes
|
||||
4. **Follow ESR rule** - Equality, Sort, Range order
|
||||
5. **Use covered queries** - When possible, avoid document fetches
|
||||
6. **Monitor index usage** - Drop unused indexes
|
||||
```javascript
|
||||
db.collection.aggregate([{ $indexStats: {} }])
|
||||
```
|
||||
7. **Partial indexes for filtered queries** - Reduce index size
|
||||
8. **Consider index size** - Should fit in RAM
|
||||
```javascript
|
||||
db.collection.stats().indexSizes
|
||||
```
|
||||
9. **Background index creation** - Don't block operations (deprecated in 4.2+)
|
||||
10. **Test with explain** - Verify query plan before production
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Over-indexing** - Too many indexes slow writes
|
||||
2. **Unused indexes** - Waste space and write performance
|
||||
3. **Regex without prefix** - `/pattern/` can't use index, `/^pattern/` can
|
||||
4. **$ne, $nin queries** - Often scan entire collection
|
||||
5. **$or with multiple branches** - May not use indexes efficiently
|
||||
6. **Sort without index** - In-memory sort limited to 32MB
|
||||
7. **Compound index order** - Wrong order makes index useless
|
||||
8. **Case-sensitive queries** - Use collation for case-insensitive
|
||||
|
||||
## Monitoring
|
||||
|
||||
```javascript
|
||||
// Current operations
|
||||
db.currentOp()
|
||||
|
||||
// Slow queries (enable profiling)
|
||||
db.setProfilingLevel(1, { slowms: 100 })
|
||||
db.system.profile.find().sort({ ts: -1 }).limit(10)
|
||||
|
||||
// Index statistics
|
||||
db.collection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
{ $sort: { "accesses.ops": -1 } }
|
||||
])
|
||||
|
||||
// Collection statistics
|
||||
db.collection.stats()
|
||||
```
|
||||
|
||||
## Index Size Calculation
|
||||
|
||||
```javascript
|
||||
// Check index sizes
|
||||
db.collection.stats().indexSizes
|
||||
|
||||
// Total index size
|
||||
db.collection.totalIndexSize()
|
||||
|
||||
// Recommend: indexes fit in RAM
|
||||
// Monitor: db.serverStatus().mem
|
||||
```
|
||||
594
skills/databases/references/postgresql-administration.md
Normal file
594
skills/databases/references/postgresql-administration.md
Normal file
@@ -0,0 +1,594 @@
|
||||
# PostgreSQL Administration
|
||||
|
||||
User management, backups, replication, maintenance, and production database administration.
|
||||
|
||||
## User and Role Management
|
||||
|
||||
### Create Users
|
||||
```sql
|
||||
-- Create user with password
|
||||
CREATE USER appuser WITH PASSWORD 'secure_password';
|
||||
|
||||
-- Create superuser
|
||||
CREATE USER admin WITH SUPERUSER PASSWORD 'admin_password';
|
||||
|
||||
-- Create role without login
|
||||
CREATE ROLE readonly;
|
||||
|
||||
-- Create user with attributes
|
||||
CREATE USER developer WITH
|
||||
PASSWORD 'dev_pass'
|
||||
CREATEDB
|
||||
VALID UNTIL '2025-12-31';
|
||||
```
|
||||
|
||||
### Alter Users
|
||||
```sql
|
||||
-- Change password
|
||||
ALTER USER appuser WITH PASSWORD 'new_password';
|
||||
|
||||
-- Add attributes
|
||||
ALTER USER appuser WITH CREATEDB CREATEROLE;
|
||||
|
||||
-- Remove attributes
|
||||
ALTER USER appuser WITH NOSUPERUSER;
|
||||
|
||||
-- Rename user
|
||||
ALTER USER oldname RENAME TO newname;
|
||||
|
||||
-- Set connection limit
|
||||
ALTER USER appuser CONNECTION LIMIT 10;
|
||||
```
|
||||
|
||||
### Roles and Inheritance
|
||||
```sql
|
||||
-- Create role hierarchy
|
||||
CREATE ROLE readonly;
|
||||
CREATE ROLE readwrite;
|
||||
|
||||
-- Grant role to user
|
||||
GRANT readonly TO appuser;
|
||||
GRANT readwrite TO developer;
|
||||
|
||||
-- Revoke role
|
||||
REVOKE readonly FROM appuser;
|
||||
|
||||
-- Role membership
|
||||
\du
|
||||
```
|
||||
|
||||
### Permissions
|
||||
|
||||
#### Database Level
|
||||
```sql
|
||||
-- Grant database access
|
||||
GRANT CONNECT ON DATABASE mydb TO appuser;
|
||||
|
||||
-- Grant schema usage
|
||||
GRANT USAGE ON SCHEMA public TO appuser;
|
||||
|
||||
-- Revoke access
|
||||
REVOKE CONNECT ON DATABASE mydb FROM appuser;
|
||||
```
|
||||
|
||||
#### Table Level
|
||||
```sql
|
||||
-- Grant table permissions
|
||||
GRANT SELECT ON users TO appuser;
|
||||
GRANT SELECT, INSERT, UPDATE ON orders TO appuser;
|
||||
GRANT ALL PRIVILEGES ON products TO appuser;
|
||||
|
||||
-- Grant on all tables
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
|
||||
|
||||
-- Revoke permissions
|
||||
REVOKE INSERT ON users FROM appuser;
|
||||
```
|
||||
|
||||
#### Column Level
|
||||
```sql
|
||||
-- Grant specific columns
|
||||
GRANT SELECT (id, name, email) ON users TO appuser;
|
||||
GRANT UPDATE (status) ON orders TO appuser;
|
||||
```
|
||||
|
||||
#### Sequence Permissions
|
||||
```sql
|
||||
-- Grant sequence usage (for SERIAL/auto-increment)
|
||||
GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO appuser;
|
||||
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO appuser;
|
||||
```
|
||||
|
||||
#### Function Permissions
|
||||
```sql
|
||||
-- Grant execute on function
|
||||
GRANT EXECUTE ON FUNCTION get_user(integer) TO appuser;
|
||||
```
|
||||
|
||||
### Default Privileges
|
||||
```sql
|
||||
-- Set default privileges for future objects
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||
GRANT SELECT ON TABLES TO readonly;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO readwrite;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||
GRANT USAGE ON SEQUENCES TO readwrite;
|
||||
```
|
||||
|
||||
### View Permissions
|
||||
```sql
|
||||
-- Show table permissions
|
||||
\dp users
|
||||
|
||||
-- Show role memberships
|
||||
\du
|
||||
|
||||
-- Query permissions
|
||||
SELECT grantee, privilege_type
|
||||
FROM information_schema.role_table_grants
|
||||
WHERE table_name = 'users';
|
||||
```
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### pg_dump (Logical Backup)
|
||||
```bash
|
||||
# Dump database to SQL file
|
||||
pg_dump mydb > mydb.sql
|
||||
|
||||
# Custom format (compressed, allows selective restore)
|
||||
pg_dump -Fc mydb > mydb.dump
|
||||
|
||||
# Directory format (parallel dump)
|
||||
pg_dump -Fd mydb -j 4 -f mydb_dir
|
||||
|
||||
# Specific table
|
||||
pg_dump -t users mydb > users.sql
|
||||
|
||||
# Multiple tables
|
||||
pg_dump -t users -t orders mydb > tables.sql
|
||||
|
||||
# Schema only
|
||||
pg_dump -s mydb > schema.sql
|
||||
|
||||
# Data only
|
||||
pg_dump -a mydb > data.sql
|
||||
|
||||
# Exclude table
|
||||
pg_dump --exclude-table=logs mydb > mydb.sql
|
||||
|
||||
# With compression
|
||||
pg_dump -Fc -Z 9 mydb > mydb.dump
|
||||
```
|
||||
|
||||
### pg_dumpall (All Databases)
|
||||
```bash
|
||||
# Dump all databases
|
||||
pg_dumpall > all_databases.sql
|
||||
|
||||
# Only globals (roles, tablespaces)
|
||||
pg_dumpall --globals-only > globals.sql
|
||||
```
|
||||
|
||||
### pg_restore
|
||||
```bash
|
||||
# Restore from custom format
|
||||
pg_restore -d mydb mydb.dump
|
||||
|
||||
# Restore specific table
|
||||
pg_restore -d mydb -t users mydb.dump
|
||||
|
||||
# List contents
|
||||
pg_restore -l mydb.dump
|
||||
|
||||
# Parallel restore
|
||||
pg_restore -d mydb -j 4 mydb.dump
|
||||
|
||||
# Clean database first
|
||||
pg_restore -d mydb --clean mydb.dump
|
||||
|
||||
# Create database if not exists
|
||||
pg_restore -C -d postgres mydb.dump
|
||||
```
|
||||
|
||||
### Restore from SQL
|
||||
```bash
|
||||
# Restore SQL dump
|
||||
psql mydb < mydb.sql
|
||||
|
||||
# Create database and restore
|
||||
createdb mydb
|
||||
psql mydb < mydb.sql
|
||||
|
||||
# Single transaction
|
||||
psql -1 mydb < mydb.sql
|
||||
|
||||
# Stop on error
|
||||
psql --set ON_ERROR_STOP=on mydb < mydb.sql
|
||||
```
|
||||
|
||||
### Automated Backup Script
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
# Configuration
|
||||
DB_NAME="mydb"
|
||||
BACKUP_DIR="/backups"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
RETENTION_DAYS=7
|
||||
|
||||
# Create backup
|
||||
pg_dump -Fc "$DB_NAME" > "$BACKUP_DIR/${DB_NAME}_${DATE}.dump"
|
||||
|
||||
# Remove old backups
|
||||
find "$BACKUP_DIR" -name "${DB_NAME}_*.dump" -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
# Log
|
||||
echo "Backup completed: ${DB_NAME}_${DATE}.dump"
|
||||
```
|
||||
|
||||
### Point-in-Time Recovery (PITR)
|
||||
```bash
|
||||
# Enable WAL archiving (postgresql.conf)
|
||||
wal_level = replica
|
||||
archive_mode = on
|
||||
archive_command = 'cp %p /archive/%f'
|
||||
max_wal_senders = 3
|
||||
|
||||
# Base backup
|
||||
pg_basebackup -D /backup/base -Ft -z -P
|
||||
|
||||
# Restore to point in time
|
||||
# 1. Stop PostgreSQL
|
||||
# 2. Restore base backup
|
||||
# 3. Create recovery.conf with recovery_target_time
|
||||
# 4. Start PostgreSQL
|
||||
```
|
||||
|
||||
## Replication
|
||||
|
||||
### Streaming Replication (Primary-Replica)
|
||||
|
||||
#### Primary Setup
|
||||
```sql
|
||||
-- Create replication user
|
||||
CREATE USER replicator WITH REPLICATION PASSWORD 'replica_pass';
|
||||
|
||||
-- Configure postgresql.conf
|
||||
wal_level = replica
|
||||
max_wal_senders = 3
|
||||
wal_keep_size = 64MB
|
||||
|
||||
-- Configure pg_hba.conf
|
||||
host replication replicator replica_ip/32 md5
|
||||
```
|
||||
|
||||
#### Replica Setup
|
||||
```bash
|
||||
# Stop replica PostgreSQL
|
||||
systemctl stop postgresql
|
||||
|
||||
# Remove data directory
|
||||
rm -rf /var/lib/postgresql/data/*
|
||||
|
||||
# Clone from primary
|
||||
pg_basebackup -h primary_host -D /var/lib/postgresql/data -U replicator -P -R
|
||||
|
||||
# Start replica
|
||||
systemctl start postgresql
|
||||
|
||||
# Check replication status
|
||||
SELECT * FROM pg_stat_replication; -- On primary
|
||||
```
|
||||
|
||||
### Logical Replication
|
||||
|
||||
#### Publisher (Primary)
|
||||
```sql
|
||||
-- Create publication
|
||||
CREATE PUBLICATION my_publication FOR ALL TABLES;
|
||||
|
||||
-- Or specific tables
|
||||
CREATE PUBLICATION my_publication FOR TABLE users, orders;
|
||||
|
||||
-- Check publications
|
||||
\dRp
|
||||
SELECT * FROM pg_publication;
|
||||
```
|
||||
|
||||
#### Subscriber (Replica)
|
||||
```sql
|
||||
-- Create subscription
|
||||
CREATE SUBSCRIPTION my_subscription
|
||||
CONNECTION 'host=primary_host dbname=mydb user=replicator password=replica_pass'
|
||||
PUBLICATION my_publication;
|
||||
|
||||
-- Check subscriptions
|
||||
\dRs
|
||||
SELECT * FROM pg_subscription;
|
||||
|
||||
-- Monitor replication
|
||||
SELECT * FROM pg_stat_subscription;
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Database Size
|
||||
```sql
|
||||
-- Database size
|
||||
SELECT pg_size_pretty(pg_database_size('mydb'));
|
||||
|
||||
-- Table sizes
|
||||
SELECT schemaname, tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
|
||||
-- Index sizes
|
||||
SELECT schemaname, tablename, indexname,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) AS size
|
||||
FROM pg_stat_user_indexes
|
||||
ORDER BY pg_relation_size(indexrelid) DESC;
|
||||
```
|
||||
|
||||
### Connections
|
||||
```sql
|
||||
-- Current connections
|
||||
SELECT count(*) FROM pg_stat_activity;
|
||||
|
||||
-- Connections by database
|
||||
SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;
|
||||
|
||||
-- Connection limit
|
||||
SHOW max_connections;
|
||||
|
||||
-- Kill connection
|
||||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = 12345;
|
||||
```
|
||||
|
||||
### Activity
|
||||
```sql
|
||||
-- Active queries
|
||||
SELECT pid, usename, state, query, query_start
|
||||
FROM pg_stat_activity
|
||||
WHERE state != 'idle';
|
||||
|
||||
-- Long-running queries
|
||||
SELECT pid, now() - query_start AS duration, query
|
||||
FROM pg_stat_activity
|
||||
WHERE state != 'idle'
|
||||
ORDER BY duration DESC;
|
||||
|
||||
-- Blocking queries
|
||||
SELECT blocked.pid AS blocked_pid,
|
||||
blocked.query AS blocked_query,
|
||||
blocking.pid AS blocking_pid,
|
||||
blocking.query AS blocking_query
|
||||
FROM pg_stat_activity blocked
|
||||
JOIN pg_stat_activity blocking
|
||||
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));
|
||||
```
|
||||
|
||||
### Cache Hit Ratio
|
||||
```sql
|
||||
-- Should be > 0.99 for good performance
|
||||
SELECT
|
||||
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_ratio
|
||||
FROM pg_statio_user_tables;
|
||||
```
|
||||
|
||||
### Table Bloat
|
||||
```sql
|
||||
-- Check for table bloat (requires pgstattuple extension)
|
||||
CREATE EXTENSION pgstattuple;
|
||||
|
||||
SELECT schemaname, tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
|
||||
pgstattuple(schemaname||'.'||tablename) AS stats
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public';
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### VACUUM
|
||||
```sql
|
||||
-- Reclaim storage
|
||||
VACUUM users;
|
||||
|
||||
-- Verbose
|
||||
VACUUM VERBOSE users;
|
||||
|
||||
-- Full (locks table, rewrites)
|
||||
VACUUM FULL users;
|
||||
|
||||
-- With analyze
|
||||
VACUUM ANALYZE users;
|
||||
|
||||
-- All tables
|
||||
VACUUM;
|
||||
```
|
||||
|
||||
### Auto-Vacuum
|
||||
```sql
|
||||
-- Check last vacuum
|
||||
SELECT schemaname, tablename, last_vacuum, last_autovacuum
|
||||
FROM pg_stat_user_tables;
|
||||
|
||||
-- Configure postgresql.conf
|
||||
autovacuum = on
|
||||
autovacuum_vacuum_threshold = 50
|
||||
autovacuum_vacuum_scale_factor = 0.2
|
||||
autovacuum_analyze_threshold = 50
|
||||
autovacuum_analyze_scale_factor = 0.1
|
||||
```
|
||||
|
||||
### REINDEX
|
||||
```sql
|
||||
-- Rebuild index
|
||||
REINDEX INDEX idx_users_email;
|
||||
|
||||
-- Rebuild all indexes on table
|
||||
REINDEX TABLE users;
|
||||
|
||||
-- Rebuild database indexes
|
||||
REINDEX DATABASE mydb;
|
||||
|
||||
-- Concurrently (doesn't lock)
|
||||
REINDEX INDEX CONCURRENTLY idx_users_email;
|
||||
```
|
||||
|
||||
### ANALYZE
|
||||
```sql
|
||||
-- Update statistics
|
||||
ANALYZE users;
|
||||
|
||||
-- Specific columns
|
||||
ANALYZE users(email, status);
|
||||
|
||||
-- All tables
|
||||
ANALYZE;
|
||||
|
||||
-- Verbose
|
||||
ANALYZE VERBOSE users;
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### postgresql.conf Location
|
||||
```sql
|
||||
SHOW config_file;
|
||||
```
|
||||
|
||||
### Key Settings
|
||||
```conf
|
||||
# Memory
|
||||
shared_buffers = 4GB # 25% of RAM
|
||||
work_mem = 64MB # Per operation
|
||||
maintenance_work_mem = 512MB # VACUUM, CREATE INDEX
|
||||
effective_cache_size = 12GB # OS cache estimate
|
||||
|
||||
# Query Planner
|
||||
random_page_cost = 1.1 # Lower for SSD
|
||||
effective_io_concurrency = 200 # Concurrent disk ops
|
||||
|
||||
# Connections
|
||||
max_connections = 100
|
||||
superuser_reserved_connections = 3
|
||||
|
||||
# Logging
|
||||
log_destination = 'stderr'
|
||||
logging_collector = on
|
||||
log_directory = 'log'
|
||||
log_filename = 'postgresql-%Y-%m-%d.log'
|
||||
log_rotation_age = 1d
|
||||
log_min_duration_statement = 100 # Log slow queries
|
||||
|
||||
# Replication
|
||||
wal_level = replica
|
||||
max_wal_senders = 3
|
||||
wal_keep_size = 64MB
|
||||
|
||||
# Autovacuum
|
||||
autovacuum = on
|
||||
autovacuum_vacuum_scale_factor = 0.2
|
||||
autovacuum_analyze_scale_factor = 0.1
|
||||
```
|
||||
|
||||
### Reload Configuration
|
||||
```sql
|
||||
-- Reload config without restart
|
||||
SELECT pg_reload_conf();
|
||||
|
||||
-- Or from shell
|
||||
pg_ctl reload
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### SSL/TLS
|
||||
```conf
|
||||
# postgresql.conf
|
||||
ssl = on
|
||||
ssl_cert_file = '/path/to/server.crt'
|
||||
ssl_key_file = '/path/to/server.key'
|
||||
ssl_ca_file = '/path/to/ca.crt'
|
||||
```
|
||||
|
||||
### pg_hba.conf (Host-Based Authentication)
|
||||
```conf
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
|
||||
# Local connections
|
||||
local all postgres peer
|
||||
local all all md5
|
||||
|
||||
# Remote connections
|
||||
host all all 0.0.0.0/0 md5
|
||||
host all all ::0/0 md5
|
||||
|
||||
# Replication
|
||||
host replication replicator replica_ip/32 md5
|
||||
|
||||
# SSL required
|
||||
hostssl all all 0.0.0.0/0 md5
|
||||
```
|
||||
|
||||
### Row Level Security
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy
|
||||
CREATE POLICY user_policy ON users
|
||||
USING (user_id = current_user_id());
|
||||
|
||||
-- Drop policy
|
||||
DROP POLICY user_policy ON users;
|
||||
|
||||
-- View policies
|
||||
\d+ users
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Backups**
|
||||
- Daily automated backups
|
||||
- Test restores regularly
|
||||
- Store backups off-site
|
||||
- Use pg_dump custom format for flexibility
|
||||
|
||||
2. **Monitoring**
|
||||
- Monitor connections, queries, cache hit ratio
|
||||
- Set up alerts for critical metrics
|
||||
- Log slow queries
|
||||
- Use pg_stat_statements
|
||||
|
||||
3. **Security**
|
||||
- Use strong passwords
|
||||
- Restrict network access (pg_hba.conf)
|
||||
- Enable SSL/TLS
|
||||
- Regular security updates
|
||||
- Principle of least privilege
|
||||
|
||||
4. **Maintenance**
|
||||
- Regular VACUUM and ANALYZE
|
||||
- Monitor autovacuum
|
||||
- REINDEX periodically
|
||||
- Check for table bloat
|
||||
|
||||
5. **Configuration**
|
||||
- Tune for workload
|
||||
- Use connection pooling (pgBouncer)
|
||||
- Monitor and adjust memory settings
|
||||
- Keep PostgreSQL updated
|
||||
|
||||
6. **Replication**
|
||||
- At least one replica for HA
|
||||
- Monitor replication lag
|
||||
- Test failover procedures
|
||||
- Use logical replication for selective replication
|
||||
527
skills/databases/references/postgresql-performance.md
Normal file
527
skills/databases/references/postgresql-performance.md
Normal file
@@ -0,0 +1,527 @@
|
||||
# PostgreSQL Performance Optimization
|
||||
|
||||
Query optimization, indexing strategies, EXPLAIN analysis, and performance tuning for PostgreSQL.
|
||||
|
||||
## EXPLAIN Command
|
||||
|
||||
### Basic EXPLAIN
|
||||
```sql
|
||||
-- Show query plan
|
||||
EXPLAIN SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- Output shows:
|
||||
-- - Execution plan nodes
|
||||
-- - Estimated costs
|
||||
-- - Estimated rows
|
||||
```
|
||||
|
||||
### EXPLAIN ANALYZE
|
||||
```sql
|
||||
-- Execute query and show actual performance
|
||||
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 18;
|
||||
|
||||
-- Shows:
|
||||
-- - Actual execution time
|
||||
-- - Actual rows returned
|
||||
-- - Planning time
|
||||
-- - Execution time
|
||||
```
|
||||
|
||||
### EXPLAIN Options
|
||||
```sql
|
||||
-- Verbose output
|
||||
EXPLAIN (VERBOSE) SELECT * FROM users;
|
||||
|
||||
-- Show buffer usage
|
||||
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE active = true;
|
||||
|
||||
-- JSON format
|
||||
EXPLAIN (FORMAT JSON, ANALYZE) SELECT * FROM users;
|
||||
|
||||
-- All options
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, TIMING, COSTS)
|
||||
SELECT * FROM users WHERE id = 1;
|
||||
```
|
||||
|
||||
## Understanding Query Plans
|
||||
|
||||
### Scan Methods
|
||||
|
||||
#### Sequential Scan
|
||||
```sql
|
||||
-- Full table scan (reads all rows)
|
||||
EXPLAIN SELECT * FROM users WHERE name = 'Alice';
|
||||
|
||||
-- Output: Seq Scan on users
|
||||
-- Indicates: no suitable index or small table
|
||||
```
|
||||
|
||||
#### Index Scan
|
||||
```sql
|
||||
-- Uses index to find rows
|
||||
EXPLAIN SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- Output: Index Scan using users_pkey on users
|
||||
-- Best for: selective queries, small result sets
|
||||
```
|
||||
|
||||
#### Index Only Scan
|
||||
```sql
|
||||
-- Query covered by index (no table access)
|
||||
CREATE INDEX idx_users_email_name ON users(email, name);
|
||||
EXPLAIN SELECT email, name FROM users WHERE email = 'alice@example.com';
|
||||
|
||||
-- Output: Index Only Scan using idx_users_email_name
|
||||
-- Best performance: no heap fetch needed
|
||||
```
|
||||
|
||||
#### Bitmap Scan
|
||||
```sql
|
||||
-- Combines multiple indexes or handles large result sets
|
||||
EXPLAIN SELECT * FROM users WHERE age > 18 AND status = 'active';
|
||||
|
||||
-- Output:
|
||||
-- Bitmap Heap Scan on users
|
||||
-- Recheck Cond: ...
|
||||
-- -> Bitmap Index Scan on idx_age
|
||||
|
||||
-- Good for: moderate selectivity
|
||||
```
|
||||
|
||||
### Join Methods
|
||||
|
||||
#### Nested Loop
|
||||
```sql
|
||||
-- For each row in outer table, scan inner table
|
||||
EXPLAIN SELECT * FROM orders o
|
||||
JOIN customers c ON o.customer_id = c.id
|
||||
WHERE c.id = 1;
|
||||
|
||||
-- Output: Nested Loop
|
||||
-- Best for: small outer table, indexed inner table
|
||||
```
|
||||
|
||||
#### Hash Join
|
||||
```sql
|
||||
-- Build hash table from smaller table
|
||||
EXPLAIN SELECT * FROM orders o
|
||||
JOIN customers c ON o.customer_id = c.id;
|
||||
|
||||
-- Output: Hash Join
|
||||
-- Best for: large tables, equality conditions
|
||||
```
|
||||
|
||||
#### Merge Join
|
||||
```sql
|
||||
-- Both inputs sorted on join key
|
||||
EXPLAIN SELECT * FROM orders o
|
||||
JOIN customers c ON o.customer_id = c.id
|
||||
ORDER BY o.customer_id;
|
||||
|
||||
-- Output: Merge Join
|
||||
-- Best for: pre-sorted data, large sorted inputs
|
||||
```
|
||||
|
||||
## Indexing Strategies
|
||||
|
||||
### B-tree Index (Default)
|
||||
```sql
|
||||
-- General purpose index
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_orders_date ON orders(order_date);
|
||||
|
||||
-- Supports: =, <, <=, >, >=, BETWEEN, IN, IS NULL
|
||||
-- Supports: ORDER BY, MIN/MAX
|
||||
```
|
||||
|
||||
### Composite Index
|
||||
```sql
|
||||
-- Multiple columns (order matters!)
|
||||
CREATE INDEX idx_users_status_created ON users(status, created_at);
|
||||
|
||||
-- Supports queries on:
|
||||
-- - status
|
||||
-- - status, created_at
|
||||
-- Does NOT support: created_at alone
|
||||
|
||||
-- Column order: most selective first
|
||||
-- Exception: match query WHERE/ORDER BY patterns
|
||||
```
|
||||
|
||||
### Partial Index
|
||||
```sql
|
||||
-- Index subset of rows
|
||||
CREATE INDEX idx_active_users ON users(email)
|
||||
WHERE status = 'active';
|
||||
|
||||
-- Smaller index, faster queries with matching WHERE clause
|
||||
-- Query must include WHERE status = 'active' to use index
|
||||
```
|
||||
|
||||
### Expression Index
|
||||
```sql
|
||||
-- Index on computed value
|
||||
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
|
||||
|
||||
-- Query must use same expression
|
||||
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
|
||||
```
|
||||
|
||||
### GIN Index (Generalized Inverted Index)
|
||||
```sql
|
||||
-- For array, JSONB, full-text search
|
||||
CREATE INDEX idx_products_tags ON products USING GIN(tags);
|
||||
CREATE INDEX idx_documents_data ON documents USING GIN(data);
|
||||
|
||||
-- Array queries
|
||||
SELECT * FROM products WHERE tags @> ARRAY['featured'];
|
||||
|
||||
-- JSONB queries
|
||||
SELECT * FROM documents WHERE data @> '{"status": "active"}';
|
||||
```
|
||||
|
||||
### GiST Index (Generalized Search Tree)
|
||||
```sql
|
||||
-- For geometric data, range types, full-text
|
||||
CREATE INDEX idx_locations_geom ON locations USING GiST(geom);
|
||||
|
||||
-- Geometric queries
|
||||
SELECT * FROM locations WHERE geom && ST_MakeEnvelope(...);
|
||||
```
|
||||
|
||||
### Hash Index
|
||||
```sql
|
||||
-- Equality comparisons only
|
||||
CREATE INDEX idx_users_hash_email ON users USING HASH(email);
|
||||
|
||||
-- Only supports: =
|
||||
-- Rarely used (B-tree usually better)
|
||||
```
|
||||
|
||||
### BRIN Index (Block Range Index)
|
||||
```sql
|
||||
-- For very large tables with natural clustering
|
||||
CREATE INDEX idx_logs_brin_created ON logs USING BRIN(created_at);
|
||||
|
||||
-- Tiny index size, good for append-only data
|
||||
-- Best for: time-series, logging, large tables
|
||||
```
|
||||
|
||||
## Query Optimization Techniques
|
||||
|
||||
### Avoid SELECT *
|
||||
```sql
|
||||
-- Bad
|
||||
SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- Good (only needed columns)
|
||||
SELECT id, name, email FROM users WHERE id = 1;
|
||||
```
|
||||
|
||||
### Use LIMIT
|
||||
```sql
|
||||
-- Limit result set
|
||||
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
|
||||
|
||||
-- PostgreSQL can stop early with LIMIT
|
||||
```
|
||||
|
||||
### Index for ORDER BY
|
||||
```sql
|
||||
-- Create index matching sort order
|
||||
CREATE INDEX idx_users_created_desc ON users(created_at DESC);
|
||||
|
||||
-- Query uses index for sorting
|
||||
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
### Covering Index
|
||||
```sql
|
||||
-- Include all queried columns in index
|
||||
CREATE INDEX idx_users_email_name_status ON users(email, name, status);
|
||||
|
||||
-- Query covered by index (no table access)
|
||||
SELECT name, status FROM users WHERE email = 'alice@example.com';
|
||||
```
|
||||
|
||||
### EXISTS vs IN
|
||||
```sql
|
||||
-- Prefer EXISTS for large subqueries
|
||||
-- Bad
|
||||
SELECT * FROM customers
|
||||
WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);
|
||||
|
||||
-- Good
|
||||
SELECT * FROM customers c
|
||||
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total > 1000);
|
||||
```
|
||||
|
||||
### JOIN Order
|
||||
```sql
|
||||
-- Filter before joining
|
||||
-- Bad
|
||||
SELECT * FROM orders o
|
||||
JOIN customers c ON o.customer_id = c.id
|
||||
WHERE o.status = 'completed' AND c.country = 'USA';
|
||||
|
||||
-- Good (filter in subquery)
|
||||
SELECT * FROM (
|
||||
SELECT * FROM orders WHERE status = 'completed'
|
||||
) o
|
||||
JOIN (
|
||||
SELECT * FROM customers WHERE country = 'USA'
|
||||
) c ON o.customer_id = c.id;
|
||||
|
||||
-- Or use CTE
|
||||
WITH filtered_orders AS (
|
||||
SELECT * FROM orders WHERE status = 'completed'
|
||||
),
|
||||
filtered_customers AS (
|
||||
SELECT * FROM customers WHERE country = 'USA'
|
||||
)
|
||||
SELECT * FROM filtered_orders o
|
||||
JOIN filtered_customers c ON o.customer_id = c.id;
|
||||
```
|
||||
|
||||
### Avoid Functions in WHERE
|
||||
```sql
|
||||
-- Bad (index not used)
|
||||
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
|
||||
|
||||
-- Good (create expression index)
|
||||
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
|
||||
-- Then query uses index
|
||||
|
||||
-- Or store lowercase separately
|
||||
ALTER TABLE users ADD COLUMN email_lower TEXT;
|
||||
UPDATE users SET email_lower = LOWER(email);
|
||||
CREATE INDEX idx_users_email_lower ON users(email_lower);
|
||||
```
|
||||
|
||||
## Statistics and ANALYZE
|
||||
|
||||
### Update Statistics
|
||||
```sql
|
||||
-- Analyze table (update statistics)
|
||||
ANALYZE users;
|
||||
|
||||
-- Analyze specific columns
|
||||
ANALYZE users(email, status);
|
||||
|
||||
-- Analyze all tables
|
||||
ANALYZE;
|
||||
|
||||
-- Auto-analyze (configured in postgresql.conf)
|
||||
autovacuum_analyze_threshold = 50
|
||||
autovacuum_analyze_scale_factor = 0.1
|
||||
```
|
||||
|
||||
### Check Statistics
|
||||
```sql
|
||||
-- Last analyze time
|
||||
SELECT schemaname, tablename, last_analyze, last_autoanalyze
|
||||
FROM pg_stat_user_tables;
|
||||
|
||||
-- Statistics targets (adjust for important columns)
|
||||
ALTER TABLE users ALTER COLUMN email SET STATISTICS 1000;
|
||||
```
|
||||
|
||||
## VACUUM and Maintenance
|
||||
|
||||
### VACUUM
|
||||
```sql
|
||||
-- Reclaim storage, update statistics
|
||||
VACUUM users;
|
||||
|
||||
-- Verbose output
|
||||
VACUUM VERBOSE users;
|
||||
|
||||
-- Full vacuum (rewrites table, locks table)
|
||||
VACUUM FULL users;
|
||||
|
||||
-- Analyze after vacuum
|
||||
VACUUM ANALYZE users;
|
||||
```
|
||||
|
||||
### Auto-Vacuum
|
||||
```sql
|
||||
-- Check autovacuum status
|
||||
SELECT schemaname, tablename, last_vacuum, last_autovacuum
|
||||
FROM pg_stat_user_tables;
|
||||
|
||||
-- Configure in postgresql.conf
|
||||
autovacuum = on
|
||||
autovacuum_vacuum_threshold = 50
|
||||
autovacuum_vacuum_scale_factor = 0.2
|
||||
```
|
||||
|
||||
### REINDEX
|
||||
```sql
|
||||
-- Rebuild index
|
||||
REINDEX INDEX idx_users_email;
|
||||
|
||||
-- Rebuild all indexes on table
|
||||
REINDEX TABLE users;
|
||||
|
||||
-- Rebuild all indexes in schema
|
||||
REINDEX SCHEMA public;
|
||||
```
|
||||
|
||||
## Monitoring Queries
|
||||
|
||||
### Active Queries
|
||||
```sql
|
||||
-- Current queries
|
||||
SELECT pid, usename, state, query, query_start
|
||||
FROM pg_stat_activity
|
||||
WHERE state != 'idle';
|
||||
|
||||
-- Long-running queries
|
||||
SELECT pid, now() - query_start AS duration, query
|
||||
FROM pg_stat_activity
|
||||
WHERE state != 'idle' AND now() - query_start > interval '5 minutes'
|
||||
ORDER BY duration DESC;
|
||||
```
|
||||
|
||||
### Slow Query Log
|
||||
```sql
|
||||
-- Enable slow query logging (postgresql.conf)
|
||||
log_min_duration_statement = 100 -- milliseconds
|
||||
|
||||
-- Or per session
|
||||
SET log_min_duration_statement = 100;
|
||||
|
||||
-- Logs appear in PostgreSQL log files
|
||||
```
|
||||
|
||||
### pg_stat_statements Extension
|
||||
```sql
|
||||
-- Enable extension
|
||||
CREATE EXTENSION pg_stat_statements;
|
||||
|
||||
-- View query statistics
|
||||
SELECT query, calls, total_exec_time, mean_exec_time, rows
|
||||
FROM pg_stat_statements
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- Reset statistics
|
||||
SELECT pg_stat_statements_reset();
|
||||
```
|
||||
|
||||
## Index Usage Analysis
|
||||
|
||||
### Check Index Usage
|
||||
```sql
|
||||
-- Index usage statistics
|
||||
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
|
||||
FROM pg_stat_user_indexes
|
||||
ORDER BY idx_scan;
|
||||
|
||||
-- Unused indexes (idx_scan = 0)
|
||||
SELECT schemaname, tablename, indexname
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE idx_scan = 0 AND indexname NOT LIKE '%_pkey';
|
||||
```
|
||||
|
||||
### Index Size
|
||||
```sql
|
||||
-- Index sizes
|
||||
SELECT schemaname, tablename, indexname,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
|
||||
FROM pg_stat_user_indexes
|
||||
ORDER BY pg_relation_size(indexrelid) DESC;
|
||||
```
|
||||
|
||||
### Missing Indexes
|
||||
```sql
|
||||
-- Tables with sequential scans
|
||||
SELECT schemaname, tablename, seq_scan, seq_tup_read
|
||||
FROM pg_stat_user_tables
|
||||
WHERE seq_scan > 0
|
||||
ORDER BY seq_tup_read DESC;
|
||||
|
||||
-- Consider adding indexes to high seq_scan tables
|
||||
```
|
||||
|
||||
## Configuration Tuning
|
||||
|
||||
### Memory Settings (postgresql.conf)
|
||||
```conf
|
||||
# Shared buffers (25% of RAM)
|
||||
shared_buffers = 4GB
|
||||
|
||||
# Work memory (per operation)
|
||||
work_mem = 64MB
|
||||
|
||||
# Maintenance work memory (VACUUM, CREATE INDEX)
|
||||
maintenance_work_mem = 512MB
|
||||
|
||||
# Effective cache size (estimate of OS cache)
|
||||
effective_cache_size = 12GB
|
||||
```
|
||||
|
||||
### Query Planner Settings
|
||||
```conf
|
||||
# Random page cost (lower for SSD)
|
||||
random_page_cost = 1.1
|
||||
|
||||
# Effective IO concurrency (number of concurrent disk operations)
|
||||
effective_io_concurrency = 200
|
||||
|
||||
# Cost of parallel query startup
|
||||
parallel_setup_cost = 1000
|
||||
parallel_tuple_cost = 0.1
|
||||
```
|
||||
|
||||
### Connection Settings
|
||||
```conf
|
||||
# Max connections
|
||||
max_connections = 100
|
||||
|
||||
# Connection pooling recommended (pgBouncer)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Index strategy**
|
||||
- Index foreign keys
|
||||
- Index WHERE clause columns
|
||||
- Index ORDER BY columns
|
||||
- Use composite indexes for multi-column queries
|
||||
- Keep index count reasonable (5-10 per table)
|
||||
|
||||
2. **Query optimization**
|
||||
- Use EXPLAIN ANALYZE
|
||||
- Avoid SELECT *
|
||||
- Use LIMIT when possible
|
||||
- Filter before joining
|
||||
- Use appropriate join type
|
||||
|
||||
3. **Statistics**
|
||||
- Regular ANALYZE
|
||||
- Increase statistics target for skewed distributions
|
||||
- Monitor autovacuum
|
||||
|
||||
4. **Monitoring**
|
||||
- Enable pg_stat_statements
|
||||
- Log slow queries
|
||||
- Monitor index usage
|
||||
- Check table bloat
|
||||
|
||||
5. **Maintenance**
|
||||
- Regular VACUUM
|
||||
- REINDEX periodically
|
||||
- Update PostgreSQL version
|
||||
- Monitor disk space
|
||||
|
||||
6. **Configuration**
|
||||
- Tune memory settings
|
||||
- Adjust for workload (OLTP vs OLAP)
|
||||
- Use connection pooling
|
||||
- Enable query logging
|
||||
|
||||
7. **Testing**
|
||||
- Test queries with production-like data volume
|
||||
- Benchmark before/after changes
|
||||
- Monitor production metrics
|
||||
467
skills/databases/references/postgresql-psql-cli.md
Normal file
467
skills/databases/references/postgresql-psql-cli.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# PostgreSQL psql CLI
|
||||
|
||||
Command-line interface for PostgreSQL: connection, meta-commands, scripting, and interactive usage.
|
||||
|
||||
## Connection
|
||||
|
||||
### Basic Connection
|
||||
```bash
|
||||
# Connect to database
|
||||
psql -U username -d database -h hostname -p 5432
|
||||
|
||||
# Connect using URI
|
||||
psql postgresql://username:password@hostname:5432/database
|
||||
|
||||
# Environment variables
|
||||
export PGUSER=postgres
|
||||
export PGPASSWORD=mypassword
|
||||
export PGHOST=localhost
|
||||
export PGPORT=5432
|
||||
export PGDATABASE=mydb
|
||||
psql
|
||||
```
|
||||
|
||||
### Password File (~/.pgpass)
|
||||
```bash
|
||||
# Format: hostname:port:database:username:password
|
||||
# chmod 600 ~/.pgpass
|
||||
localhost:5432:mydb:postgres:mypassword
|
||||
*.example.com:5432:*:appuser:apppass
|
||||
```
|
||||
|
||||
### SSL Connection
|
||||
```bash
|
||||
# Require SSL
|
||||
psql "host=hostname sslmode=require user=username dbname=database"
|
||||
|
||||
# Verify certificate
|
||||
psql "host=hostname sslmode=verify-full \
|
||||
sslcert=/path/to/client.crt \
|
||||
sslkey=/path/to/client.key \
|
||||
sslrootcert=/path/to/ca.crt"
|
||||
```
|
||||
|
||||
## Essential Meta-Commands
|
||||
|
||||
### Database Navigation
|
||||
```bash
|
||||
\l or \list # List databases
|
||||
\l+ # List with sizes
|
||||
\c database # Connect to database
|
||||
\c database username # Connect as user
|
||||
\conninfo # Connection info
|
||||
```
|
||||
|
||||
### Schema Inspection
|
||||
```bash
|
||||
\dn # List schemas
|
||||
\dt # List tables
|
||||
\dt+ # Tables with sizes
|
||||
\dt *.* # All tables, all schemas
|
||||
\di # List indexes
|
||||
\dv # List views
|
||||
\dm # List materialized views
|
||||
\ds # List sequences
|
||||
\df # List functions
|
||||
```
|
||||
|
||||
### Object Description
|
||||
```bash
|
||||
\d tablename # Describe table
|
||||
\d+ tablename # Detailed description
|
||||
\d indexname # Describe index
|
||||
\df functionname # Describe function
|
||||
\du # List users/roles
|
||||
\dp tablename # Show permissions
|
||||
```
|
||||
|
||||
### Output Formatting
|
||||
```bash
|
||||
\x # Toggle expanded output
|
||||
\x on # Enable expanded
|
||||
\x off # Disable expanded
|
||||
\a # Toggle aligned output
|
||||
\t # Toggle tuples only
|
||||
\H # HTML output
|
||||
\pset format csv # CSV format
|
||||
\pset null '[NULL]' # Show NULL values
|
||||
```
|
||||
|
||||
### Execution Commands
|
||||
```bash
|
||||
\i filename.sql # Execute SQL file
|
||||
\o output.txt # Redirect output to file
|
||||
\o # Stop redirecting
|
||||
\! command # Execute shell command
|
||||
\timing # Toggle timing
|
||||
\q # Quit
|
||||
```
|
||||
|
||||
## psql Command-Line Options
|
||||
|
||||
```bash
|
||||
# Connection
|
||||
-h hostname # Host
|
||||
-p port # Port (default 5432)
|
||||
-U username # Username
|
||||
-d database # Database
|
||||
-W # Prompt for password
|
||||
|
||||
# Execution
|
||||
-c "SQL" # Execute command and exit
|
||||
-f file.sql # Execute file
|
||||
--command="SQL" # Execute command
|
||||
|
||||
# Output
|
||||
-t # Tuples only (no headers)
|
||||
-A # Unaligned output
|
||||
-F "," # Field separator
|
||||
-o output.txt # Output to file
|
||||
-q # Quiet mode
|
||||
-x # Expanded output
|
||||
|
||||
# Script options
|
||||
-1 # Execute as transaction
|
||||
--on-error-stop # Stop on error
|
||||
-v variable=value # Set variable
|
||||
-L logfile.log # Log session
|
||||
```
|
||||
|
||||
## Running SQL
|
||||
|
||||
### Interactive Queries
|
||||
```sql
|
||||
-- Simple query
|
||||
SELECT * FROM users;
|
||||
|
||||
-- Multi-line (ends with semicolon)
|
||||
SELECT id, name, email
|
||||
FROM users
|
||||
WHERE active = true;
|
||||
|
||||
-- Edit in editor
|
||||
\e
|
||||
|
||||
-- Repeat last query
|
||||
\g
|
||||
|
||||
-- Send to file
|
||||
\g output.txt
|
||||
```
|
||||
|
||||
### Variables
|
||||
```bash
|
||||
# Set variable
|
||||
\set myvar 'value'
|
||||
\set limit 10
|
||||
|
||||
# Use variable
|
||||
SELECT * FROM users LIMIT :limit;
|
||||
|
||||
# String variable (quoted)
|
||||
\set username 'alice'
|
||||
SELECT * FROM users WHERE name = :'username';
|
||||
|
||||
# Show all variables
|
||||
\set
|
||||
|
||||
# Unset variable
|
||||
\unset myvar
|
||||
```
|
||||
|
||||
### Scripts
|
||||
```sql
|
||||
-- script.sql
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE
|
||||
);
|
||||
|
||||
INSERT INTO users (name, email) VALUES
|
||||
('Alice', 'alice@example.com'),
|
||||
('Bob', 'bob@example.com');
|
||||
|
||||
COMMIT;
|
||||
|
||||
\echo 'Script completed!'
|
||||
```
|
||||
|
||||
```bash
|
||||
# Execute script
|
||||
psql -d mydb -f script.sql
|
||||
|
||||
# With error stopping
|
||||
psql -d mydb -f script.sql --on-error-stop
|
||||
|
||||
# In single transaction
|
||||
psql -d mydb -1 -f script.sql
|
||||
```
|
||||
|
||||
## Data Import/Export
|
||||
|
||||
### COPY (Server-side)
|
||||
```sql
|
||||
-- Export to CSV
|
||||
COPY users TO '/tmp/users.csv' WITH (FORMAT CSV, HEADER);
|
||||
|
||||
-- Import from CSV
|
||||
COPY users FROM '/tmp/users.csv' WITH (FORMAT CSV, HEADER);
|
||||
|
||||
-- Query to file
|
||||
COPY (SELECT * FROM users WHERE active = true)
|
||||
TO '/tmp/active_users.csv' WITH (FORMAT CSV, HEADER);
|
||||
```
|
||||
|
||||
### \copy (Client-side)
|
||||
```bash
|
||||
# Export (from psql)
|
||||
\copy users TO 'users.csv' WITH (FORMAT CSV, HEADER)
|
||||
|
||||
# Export query results
|
||||
\copy (SELECT * FROM users WHERE active = true) TO 'active.csv' CSV HEADER
|
||||
|
||||
# Import
|
||||
\copy users FROM 'users.csv' WITH (FORMAT CSV, HEADER)
|
||||
|
||||
# To stdout
|
||||
\copy users TO STDOUT CSV HEADER > users.csv
|
||||
```
|
||||
|
||||
### pg_dump / pg_restore
|
||||
```bash
|
||||
# Dump database
|
||||
pg_dump mydb > mydb.sql
|
||||
pg_dump -d mydb -Fc > mydb.dump # Custom format
|
||||
|
||||
# Dump specific table
|
||||
pg_dump -t users mydb > users.sql
|
||||
|
||||
# Schema only
|
||||
pg_dump -s mydb > schema.sql
|
||||
|
||||
# Data only
|
||||
pg_dump -a mydb > data.sql
|
||||
|
||||
# Restore
|
||||
psql mydb < mydb.sql
|
||||
pg_restore -d mydb mydb.dump
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### ~/.psqlrc
|
||||
```bash
|
||||
# Auto-loaded on psql startup
|
||||
\set QUIET ON
|
||||
|
||||
-- Prompt customization
|
||||
\set PROMPT1 '%n@%m:%>/%/%R%# '
|
||||
|
||||
-- Output settings
|
||||
\pset null '[NULL]'
|
||||
\pset border 2
|
||||
\pset linestyle unicode
|
||||
\pset expanded auto
|
||||
|
||||
-- Timing
|
||||
\timing ON
|
||||
|
||||
-- Pager
|
||||
\pset pager always
|
||||
|
||||
-- History
|
||||
\set HISTSIZE 10000
|
||||
|
||||
-- Custom shortcuts
|
||||
\set active_users 'SELECT * FROM users WHERE status = ''active'';'
|
||||
\set dbsize 'SELECT pg_size_pretty(pg_database_size(current_database()));'
|
||||
|
||||
\set QUIET OFF
|
||||
```
|
||||
|
||||
### Useful Aliases
|
||||
```bash
|
||||
# Add to ~/.psqlrc
|
||||
\set locks 'SELECT pid, usename, pg_blocking_pids(pid) as blocked_by, query FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;'
|
||||
|
||||
\set activity 'SELECT pid, usename, state, query FROM pg_stat_activity WHERE state != ''idle'';'
|
||||
|
||||
\set table_sizes 'SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||''.''||tablename)) FROM pg_tables ORDER BY pg_total_relation_size(schemaname||''.''||tablename) DESC;'
|
||||
|
||||
\set index_usage 'SELECT schemaname, tablename, indexname, idx_scan FROM pg_stat_user_indexes ORDER BY idx_scan;'
|
||||
|
||||
# Usage: :locks, :activity, :table_sizes
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
```sql
|
||||
-- Begin transaction
|
||||
BEGIN;
|
||||
|
||||
-- Or
|
||||
START TRANSACTION;
|
||||
|
||||
-- Savepoint
|
||||
SAVEPOINT sp1;
|
||||
|
||||
-- Rollback to savepoint
|
||||
ROLLBACK TO sp1;
|
||||
|
||||
-- Commit
|
||||
COMMIT;
|
||||
|
||||
-- Rollback
|
||||
ROLLBACK;
|
||||
```
|
||||
|
||||
## Performance Analysis
|
||||
|
||||
### EXPLAIN
|
||||
```sql
|
||||
-- Show query plan
|
||||
EXPLAIN SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- With execution
|
||||
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 18;
|
||||
|
||||
-- Verbose
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
|
||||
SELECT * FROM users WHERE active = true;
|
||||
```
|
||||
|
||||
### Current Activity
|
||||
```sql
|
||||
-- Active queries
|
||||
SELECT pid, usename, state, query
|
||||
FROM pg_stat_activity;
|
||||
|
||||
-- Long-running queries
|
||||
SELECT pid, now() - query_start AS duration, query
|
||||
FROM pg_stat_activity
|
||||
WHERE state != 'idle'
|
||||
ORDER BY duration DESC;
|
||||
|
||||
-- Blocking queries
|
||||
SELECT blocked.pid, blocking.pid AS blocking_pid,
|
||||
blocked.query AS blocked_query,
|
||||
blocking.query AS blocking_query
|
||||
FROM pg_stat_activity blocked
|
||||
JOIN pg_stat_activity blocking
|
||||
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));
|
||||
```
|
||||
|
||||
### Statistics
|
||||
```sql
|
||||
-- Database size
|
||||
SELECT pg_size_pretty(pg_database_size(current_database()));
|
||||
|
||||
-- Table sizes
|
||||
SELECT schemaname, tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
|
||||
-- Index usage
|
||||
SELECT schemaname, tablename, indexname, idx_scan
|
||||
FROM pg_stat_user_indexes
|
||||
ORDER BY idx_scan;
|
||||
```
|
||||
|
||||
## User Management
|
||||
|
||||
```sql
|
||||
-- Create user
|
||||
CREATE USER appuser WITH PASSWORD 'secure_password';
|
||||
|
||||
-- Create superuser
|
||||
CREATE USER admin WITH PASSWORD 'password' SUPERUSER;
|
||||
|
||||
-- Alter user
|
||||
ALTER USER appuser WITH PASSWORD 'new_password';
|
||||
|
||||
-- Grant permissions
|
||||
GRANT CONNECT ON DATABASE mydb TO appuser;
|
||||
GRANT USAGE ON SCHEMA public TO appuser;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO appuser;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO appuser;
|
||||
|
||||
-- Default privileges
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||
GRANT SELECT ON TABLES TO appuser;
|
||||
|
||||
-- View permissions
|
||||
\dp users
|
||||
|
||||
-- Drop user
|
||||
DROP USER appuser;
|
||||
```
|
||||
|
||||
## Backup Patterns
|
||||
|
||||
```bash
|
||||
# Daily backup script
|
||||
#!/bin/bash
|
||||
DATE=$(date +%Y%m%d)
|
||||
pg_dump -Fc mydb > /backups/mydb_$DATE.dump
|
||||
|
||||
# Restore latest
|
||||
pg_restore -d mydb /backups/mydb_latest.dump
|
||||
|
||||
# Backup all databases
|
||||
pg_dumpall > /backups/all_databases.sql
|
||||
|
||||
# Backup specific schema
|
||||
pg_dump -n public mydb > public_schema.sql
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
```bash
|
||||
# Test connection
|
||||
psql -h hostname -U username -d postgres -c "SELECT 1;"
|
||||
|
||||
# Check pg_hba.conf
|
||||
# /var/lib/postgresql/data/pg_hba.conf
|
||||
|
||||
# Verbose connection
|
||||
psql -h hostname -d mydb --echo-all
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
```sql
|
||||
-- Enable slow query logging
|
||||
ALTER DATABASE mydb SET log_min_duration_statement = 100;
|
||||
|
||||
-- Check cache hit ratio
|
||||
SELECT
|
||||
sum(heap_blks_read) as heap_read,
|
||||
sum(heap_blks_hit) as heap_hit,
|
||||
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS ratio
|
||||
FROM pg_statio_user_tables;
|
||||
|
||||
-- Find slow queries
|
||||
SELECT query, mean_exec_time, calls
|
||||
FROM pg_stat_statements
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use .pgpass** for credential management
|
||||
2. **Set ON_ERROR_STOP** in scripts
|
||||
3. **Use transactions** for multi-statement changes
|
||||
4. **Test with EXPLAIN** before running expensive queries
|
||||
5. **Use \timing** to measure query performance
|
||||
6. **Configure ~/.psqlrc** for productivity
|
||||
7. **Use variables** for dynamic queries
|
||||
8. **Log sessions** with -L for auditing
|
||||
9. **Use \copy** instead of COPY for client operations
|
||||
10. **Regular backups** with pg_dump
|
||||
475
skills/databases/references/postgresql-queries.md
Normal file
475
skills/databases/references/postgresql-queries.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# PostgreSQL SQL Queries
|
||||
|
||||
SQL queries in PostgreSQL: SELECT, JOINs, subqueries, CTEs, window functions, and advanced patterns.
|
||||
|
||||
## Basic SELECT
|
||||
|
||||
### Simple Queries
|
||||
```sql
|
||||
-- Select all columns
|
||||
SELECT * FROM users;
|
||||
|
||||
-- Select specific columns
|
||||
SELECT id, name, email FROM users;
|
||||
|
||||
-- With alias
|
||||
SELECT name AS full_name, email AS contact_email FROM users;
|
||||
|
||||
-- Distinct values
|
||||
SELECT DISTINCT status FROM orders;
|
||||
|
||||
-- Count rows
|
||||
SELECT COUNT(*) FROM users;
|
||||
SELECT COUNT(DISTINCT status) FROM orders;
|
||||
```
|
||||
|
||||
### WHERE Clause
|
||||
```sql
|
||||
-- Equality
|
||||
SELECT * FROM users WHERE status = 'active';
|
||||
|
||||
-- Comparison
|
||||
SELECT * FROM products WHERE price > 100;
|
||||
SELECT * FROM orders WHERE total BETWEEN 100 AND 500;
|
||||
|
||||
-- Pattern matching
|
||||
SELECT * FROM users WHERE email LIKE '%@example.com';
|
||||
SELECT * FROM users WHERE name ILIKE 'john%'; -- case-insensitive
|
||||
|
||||
-- IN operator
|
||||
SELECT * FROM orders WHERE status IN ('pending', 'processing');
|
||||
|
||||
-- NULL checks
|
||||
SELECT * FROM users WHERE deleted_at IS NULL;
|
||||
SELECT * FROM users WHERE phone_number IS NOT NULL;
|
||||
|
||||
-- Logical operators
|
||||
SELECT * FROM products WHERE price > 100 AND stock > 0;
|
||||
SELECT * FROM users WHERE status = 'active' OR verified = true;
|
||||
SELECT * FROM products WHERE NOT (price > 1000);
|
||||
```
|
||||
|
||||
### ORDER BY
|
||||
```sql
|
||||
-- Ascending (default)
|
||||
SELECT * FROM users ORDER BY created_at;
|
||||
|
||||
-- Descending
|
||||
SELECT * FROM users ORDER BY created_at DESC;
|
||||
|
||||
-- Multiple columns
|
||||
SELECT * FROM orders ORDER BY status ASC, created_at DESC;
|
||||
|
||||
-- NULL handling
|
||||
SELECT * FROM users ORDER BY last_login NULLS FIRST;
|
||||
SELECT * FROM users ORDER BY last_login NULLS LAST;
|
||||
```
|
||||
|
||||
### LIMIT and OFFSET
|
||||
```sql
|
||||
-- Limit results
|
||||
SELECT * FROM users LIMIT 10;
|
||||
|
||||
-- Pagination
|
||||
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
|
||||
|
||||
-- Alternative: FETCH
|
||||
SELECT * FROM users OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
|
||||
```
|
||||
|
||||
## JOINs
|
||||
|
||||
### INNER JOIN
|
||||
```sql
|
||||
-- Match rows from both tables
|
||||
SELECT orders.id, orders.total, customers.name
|
||||
FROM orders
|
||||
INNER JOIN customers ON orders.customer_id = customers.id;
|
||||
|
||||
-- Short syntax
|
||||
SELECT o.id, o.total, c.name
|
||||
FROM orders o
|
||||
JOIN customers c ON o.customer_id = c.id;
|
||||
|
||||
-- Multiple joins
|
||||
SELECT o.id, c.name, p.name AS product
|
||||
FROM orders o
|
||||
JOIN customers c ON o.customer_id = c.id
|
||||
JOIN order_items oi ON oi.order_id = o.id
|
||||
JOIN products p ON oi.product_id = p.id;
|
||||
```
|
||||
|
||||
### LEFT JOIN (LEFT OUTER JOIN)
|
||||
```sql
|
||||
-- All rows from left table, matching rows from right
|
||||
SELECT c.name, o.id AS order_id
|
||||
FROM customers c
|
||||
LEFT JOIN orders o ON c.id = o.customer_id;
|
||||
|
||||
-- Find customers without orders
|
||||
SELECT c.name
|
||||
FROM customers c
|
||||
LEFT JOIN orders o ON c.id = o.customer_id
|
||||
WHERE o.id IS NULL;
|
||||
```
|
||||
|
||||
### RIGHT JOIN (RIGHT OUTER JOIN)
|
||||
```sql
|
||||
-- All rows from right table, matching rows from left
|
||||
SELECT c.name, o.id AS order_id
|
||||
FROM orders o
|
||||
RIGHT JOIN customers c ON o.customer_id = c.id;
|
||||
```
|
||||
|
||||
### FULL OUTER JOIN
|
||||
```sql
|
||||
-- All rows from both tables
|
||||
SELECT c.name, o.id AS order_id
|
||||
FROM customers c
|
||||
FULL OUTER JOIN orders o ON c.id = o.customer_id;
|
||||
```
|
||||
|
||||
### CROSS JOIN
|
||||
```sql
|
||||
-- Cartesian product (all combinations)
|
||||
SELECT c.name, p.name
|
||||
FROM colors c
|
||||
CROSS JOIN products p;
|
||||
```
|
||||
|
||||
### Self Join
|
||||
```sql
|
||||
-- Join table to itself
|
||||
SELECT e1.name AS employee, e2.name AS manager
|
||||
FROM employees e1
|
||||
LEFT JOIN employees e2 ON e1.manager_id = e2.id;
|
||||
```
|
||||
|
||||
## Subqueries
|
||||
|
||||
### Scalar Subquery
|
||||
```sql
|
||||
-- Return single value
|
||||
SELECT name, salary,
|
||||
(SELECT AVG(salary) FROM employees) AS avg_salary
|
||||
FROM employees;
|
||||
```
|
||||
|
||||
### IN Subquery
|
||||
```sql
|
||||
-- Match against set of values
|
||||
SELECT name FROM customers
|
||||
WHERE id IN (
|
||||
SELECT customer_id FROM orders WHERE total > 1000
|
||||
);
|
||||
```
|
||||
|
||||
### EXISTS Subquery
|
||||
```sql
|
||||
-- Check if subquery returns any rows
|
||||
SELECT name FROM customers c
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM orders o WHERE o.customer_id = c.id
|
||||
);
|
||||
|
||||
-- NOT EXISTS
|
||||
SELECT name FROM customers c
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM orders o WHERE o.customer_id = c.id
|
||||
);
|
||||
```
|
||||
|
||||
### Correlated Subquery
|
||||
```sql
|
||||
-- Subquery references outer query
|
||||
SELECT name, salary FROM employees e1
|
||||
WHERE salary > (
|
||||
SELECT AVG(salary) FROM employees e2
|
||||
WHERE e2.department_id = e1.department_id
|
||||
);
|
||||
```
|
||||
|
||||
## Common Table Expressions (CTEs)
|
||||
|
||||
### Simple CTE
|
||||
```sql
|
||||
-- Named temporary result set
|
||||
WITH active_users AS (
|
||||
SELECT id, name, email FROM users WHERE status = 'active'
|
||||
)
|
||||
SELECT * FROM active_users WHERE created_at > '2024-01-01';
|
||||
```
|
||||
|
||||
### Multiple CTEs
|
||||
```sql
|
||||
WITH
|
||||
active_customers AS (
|
||||
SELECT id, name FROM customers WHERE active = true
|
||||
),
|
||||
recent_orders AS (
|
||||
SELECT customer_id, SUM(total) AS total_spent
|
||||
FROM orders
|
||||
WHERE order_date > CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY customer_id
|
||||
)
|
||||
SELECT c.name, COALESCE(o.total_spent, 0) AS spent
|
||||
FROM active_customers c
|
||||
LEFT JOIN recent_orders o ON c.id = o.customer_id;
|
||||
```
|
||||
|
||||
### Recursive CTE
|
||||
```sql
|
||||
-- Tree traversal, hierarchical data
|
||||
WITH RECURSIVE category_tree AS (
|
||||
-- Base case: root categories
|
||||
SELECT id, name, parent_id, 0 AS level
|
||||
FROM categories
|
||||
WHERE parent_id IS NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Recursive case: child categories
|
||||
SELECT c.id, c.name, c.parent_id, ct.level + 1
|
||||
FROM categories c
|
||||
JOIN category_tree ct ON c.parent_id = ct.id
|
||||
)
|
||||
SELECT * FROM category_tree ORDER BY level, name;
|
||||
|
||||
-- Employee hierarchy
|
||||
WITH RECURSIVE org_chart AS (
|
||||
SELECT id, name, manager_id, 1 AS level
|
||||
FROM employees
|
||||
WHERE manager_id IS NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT e.id, e.name, e.manager_id, oc.level + 1
|
||||
FROM employees e
|
||||
JOIN org_chart oc ON e.manager_id = oc.id
|
||||
)
|
||||
SELECT * FROM org_chart;
|
||||
```
|
||||
|
||||
## Aggregate Functions
|
||||
|
||||
### Basic Aggregates
|
||||
```sql
|
||||
-- COUNT, SUM, AVG, MIN, MAX
|
||||
SELECT
|
||||
COUNT(*) AS total_orders,
|
||||
SUM(total) AS total_revenue,
|
||||
AVG(total) AS avg_order_value,
|
||||
MIN(total) AS min_order,
|
||||
MAX(total) AS max_order
|
||||
FROM orders;
|
||||
|
||||
-- COUNT variations
|
||||
SELECT COUNT(*) FROM users; -- All rows
|
||||
SELECT COUNT(phone_number) FROM users; -- Non-NULL values
|
||||
SELECT COUNT(DISTINCT status) FROM orders; -- Unique values
|
||||
```
|
||||
|
||||
### GROUP BY
|
||||
```sql
|
||||
-- Aggregate by groups
|
||||
SELECT status, COUNT(*) AS count
|
||||
FROM orders
|
||||
GROUP BY status;
|
||||
|
||||
-- Multiple grouping columns
|
||||
SELECT customer_id, status, COUNT(*) AS count
|
||||
FROM orders
|
||||
GROUP BY customer_id, status;
|
||||
|
||||
-- With aggregate functions
|
||||
SELECT customer_id,
|
||||
COUNT(*) AS order_count,
|
||||
SUM(total) AS total_spent,
|
||||
AVG(total) AS avg_order
|
||||
FROM orders
|
||||
GROUP BY customer_id;
|
||||
```
|
||||
|
||||
### HAVING
|
||||
```sql
|
||||
-- Filter after aggregation
|
||||
SELECT customer_id, SUM(total) AS total_spent
|
||||
FROM orders
|
||||
GROUP BY customer_id
|
||||
HAVING SUM(total) > 1000;
|
||||
|
||||
-- Multiple conditions
|
||||
SELECT status, COUNT(*) AS count
|
||||
FROM orders
|
||||
GROUP BY status
|
||||
HAVING COUNT(*) > 10;
|
||||
```
|
||||
|
||||
## Window Functions
|
||||
|
||||
### ROW_NUMBER
|
||||
```sql
|
||||
-- Assign unique number to each row
|
||||
SELECT id, name, salary,
|
||||
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rank
|
||||
FROM employees;
|
||||
|
||||
-- Partition by group
|
||||
SELECT id, department, salary,
|
||||
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
|
||||
FROM employees;
|
||||
```
|
||||
|
||||
### RANK / DENSE_RANK
|
||||
```sql
|
||||
-- RANK: gaps in ranking for ties
|
||||
-- DENSE_RANK: no gaps
|
||||
SELECT id, name, salary,
|
||||
RANK() OVER (ORDER BY salary DESC) AS rank,
|
||||
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
|
||||
FROM employees;
|
||||
```
|
||||
|
||||
### LAG / LEAD
|
||||
```sql
|
||||
-- Access previous/next row
|
||||
SELECT date, revenue,
|
||||
LAG(revenue) OVER (ORDER BY date) AS prev_revenue,
|
||||
LEAD(revenue) OVER (ORDER BY date) AS next_revenue,
|
||||
revenue - LAG(revenue) OVER (ORDER BY date) AS change
|
||||
FROM daily_sales;
|
||||
```
|
||||
|
||||
### Running Totals
|
||||
```sql
|
||||
-- Cumulative sum
|
||||
SELECT date, amount,
|
||||
SUM(amount) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
|
||||
FROM transactions;
|
||||
|
||||
-- Simpler syntax
|
||||
SELECT date, amount,
|
||||
SUM(amount) OVER (ORDER BY date) AS running_total
|
||||
FROM transactions;
|
||||
```
|
||||
|
||||
### Moving Averages
|
||||
```sql
|
||||
-- 7-day moving average
|
||||
SELECT date, value,
|
||||
AVG(value) OVER (
|
||||
ORDER BY date
|
||||
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
|
||||
) AS moving_avg_7d
|
||||
FROM metrics;
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### CASE Expressions
|
||||
```sql
|
||||
-- Simple CASE
|
||||
SELECT name,
|
||||
CASE status
|
||||
WHEN 'active' THEN 'Active User'
|
||||
WHEN 'pending' THEN 'Pending Verification'
|
||||
ELSE 'Inactive'
|
||||
END AS status_label
|
||||
FROM users;
|
||||
|
||||
-- Searched CASE
|
||||
SELECT name, age,
|
||||
CASE
|
||||
WHEN age < 18 THEN 'Minor'
|
||||
WHEN age BETWEEN 18 AND 65 THEN 'Adult'
|
||||
ELSE 'Senior'
|
||||
END AS age_group
|
||||
FROM users;
|
||||
```
|
||||
|
||||
### COALESCE
|
||||
```sql
|
||||
-- Return first non-NULL value
|
||||
SELECT name, COALESCE(phone_number, email, 'No contact') AS contact
|
||||
FROM users;
|
||||
```
|
||||
|
||||
### NULLIF
|
||||
```sql
|
||||
-- Return NULL if values equal
|
||||
SELECT name, NULLIF(status, 'deleted') AS active_status
|
||||
FROM users;
|
||||
```
|
||||
|
||||
### Array Operations
|
||||
```sql
|
||||
-- Array aggregate
|
||||
SELECT customer_id, ARRAY_AGG(product_id) AS products
|
||||
FROM order_items
|
||||
GROUP BY customer_id;
|
||||
|
||||
-- Unnest array
|
||||
SELECT unnest(ARRAY[1, 2, 3, 4, 5]);
|
||||
|
||||
-- Array contains
|
||||
SELECT * FROM products WHERE tags @> ARRAY['featured'];
|
||||
```
|
||||
|
||||
### JSON Operations
|
||||
```sql
|
||||
-- Query JSON/JSONB
|
||||
SELECT data->>'name' AS name FROM documents;
|
||||
SELECT data->'address'->>'city' AS city FROM documents;
|
||||
|
||||
-- Check key exists
|
||||
SELECT * FROM documents WHERE data ? 'email';
|
||||
|
||||
-- JSONB operators
|
||||
SELECT * FROM documents WHERE data @> '{"status": "active"}';
|
||||
|
||||
-- JSON aggregation
|
||||
SELECT json_agg(name) FROM users;
|
||||
SELECT json_object_agg(id, name) FROM users;
|
||||
```
|
||||
|
||||
## Set Operations
|
||||
|
||||
### UNION
|
||||
```sql
|
||||
-- Combine results (removes duplicates)
|
||||
SELECT name FROM customers
|
||||
UNION
|
||||
SELECT name FROM suppliers;
|
||||
|
||||
-- Keep duplicates
|
||||
SELECT name FROM customers
|
||||
UNION ALL
|
||||
SELECT name FROM suppliers;
|
||||
```
|
||||
|
||||
### INTERSECT
|
||||
```sql
|
||||
-- Common rows
|
||||
SELECT email FROM users
|
||||
INTERSECT
|
||||
SELECT email FROM subscribers;
|
||||
```
|
||||
|
||||
### EXCEPT
|
||||
```sql
|
||||
-- Rows in first query but not second
|
||||
SELECT email FROM users
|
||||
EXCEPT
|
||||
SELECT email FROM unsubscribed;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use indexes** on WHERE, JOIN, ORDER BY columns
|
||||
2. **Avoid SELECT *** - specify needed columns
|
||||
3. **Use EXISTS** instead of IN for large subqueries
|
||||
4. **Filter early** - WHERE before JOIN when possible
|
||||
5. **Use CTEs** for readability over nested subqueries
|
||||
6. **Parameterize queries** - prevent SQL injection
|
||||
7. **Use window functions** instead of self-joins
|
||||
8. **Test with EXPLAIN** - analyze query plans
|
||||
502
skills/databases/scripts/db_backup.py
Normal file
502
skills/databases/scripts/db_backup.py
Normal file
@@ -0,0 +1,502 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database backup and restore tool for MongoDB and PostgreSQL.
|
||||
Supports compression, scheduling, and verification.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class BackupInfo:
|
||||
"""Backup metadata."""
|
||||
|
||||
filename: str
|
||||
database_type: str
|
||||
database_name: str
|
||||
timestamp: datetime
|
||||
size_bytes: int
|
||||
compressed: bool
|
||||
verified: bool = False
|
||||
|
||||
|
||||
class BackupManager:
|
||||
"""Manages database backups for MongoDB and PostgreSQL."""
|
||||
|
||||
def __init__(self, db_type: str, backup_dir: str = "./backups"):
|
||||
"""
|
||||
Initialize backup manager.
|
||||
|
||||
Args:
|
||||
db_type: Database type ('mongodb' or 'postgres')
|
||||
backup_dir: Directory to store backups
|
||||
"""
|
||||
self.db_type = db_type.lower()
|
||||
self.backup_dir = Path(backup_dir)
|
||||
self.backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
def create_backup(
|
||||
self,
|
||||
uri: str,
|
||||
database: Optional[str] = None,
|
||||
compress: bool = True,
|
||||
verify: bool = True
|
||||
) -> Optional[BackupInfo]:
|
||||
"""
|
||||
Create database backup.
|
||||
|
||||
Args:
|
||||
uri: Database connection string
|
||||
database: Database name (optional for MongoDB)
|
||||
compress: Compress backup file
|
||||
verify: Verify backup after creation
|
||||
|
||||
Returns:
|
||||
BackupInfo if successful, None otherwise
|
||||
"""
|
||||
timestamp = datetime.now()
|
||||
date_str = timestamp.strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
if self.db_type == "mongodb":
|
||||
return self._backup_mongodb(uri, database, date_str, compress, verify)
|
||||
elif self.db_type == "postgres":
|
||||
return self._backup_postgres(uri, database, date_str, compress, verify)
|
||||
else:
|
||||
print(f"Error: Unsupported database type: {self.db_type}")
|
||||
return None
|
||||
|
||||
def _backup_mongodb(
|
||||
self,
|
||||
uri: str,
|
||||
database: Optional[str],
|
||||
date_str: str,
|
||||
compress: bool,
|
||||
verify: bool
|
||||
) -> Optional[BackupInfo]:
|
||||
"""Create MongoDB backup using mongodump."""
|
||||
db_name = database or "all"
|
||||
filename = f"mongodb_{db_name}_{date_str}"
|
||||
backup_path = self.backup_dir / filename
|
||||
|
||||
try:
|
||||
cmd = ["mongodump", "--uri", uri, "--out", str(backup_path)]
|
||||
|
||||
if database:
|
||||
cmd.extend(["--db", database])
|
||||
|
||||
print(f"Creating MongoDB backup: {filename}")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error: {result.stderr}")
|
||||
return None
|
||||
|
||||
# Compress if requested
|
||||
if compress:
|
||||
archive_path = backup_path.with_suffix(".tar.gz")
|
||||
print(f"Compressing backup...")
|
||||
shutil.make_archive(str(backup_path), "gztar", backup_path)
|
||||
shutil.rmtree(backup_path)
|
||||
backup_path = archive_path
|
||||
filename = archive_path.name
|
||||
|
||||
size_bytes = self._get_size(backup_path)
|
||||
|
||||
backup_info = BackupInfo(
|
||||
filename=filename,
|
||||
database_type="mongodb",
|
||||
database_name=db_name,
|
||||
timestamp=datetime.now(),
|
||||
size_bytes=size_bytes,
|
||||
compressed=compress
|
||||
)
|
||||
|
||||
if verify:
|
||||
backup_info.verified = self._verify_backup(backup_info)
|
||||
|
||||
self._save_metadata(backup_info)
|
||||
print(f"✓ Backup created: {filename} ({self._format_size(size_bytes)})")
|
||||
|
||||
return backup_info
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating MongoDB backup: {e}")
|
||||
return None
|
||||
|
||||
def _backup_postgres(
|
||||
self,
|
||||
uri: str,
|
||||
database: str,
|
||||
date_str: str,
|
||||
compress: bool,
|
||||
verify: bool
|
||||
) -> Optional[BackupInfo]:
|
||||
"""Create PostgreSQL backup using pg_dump."""
|
||||
if not database:
|
||||
print("Error: Database name required for PostgreSQL backup")
|
||||
return None
|
||||
|
||||
ext = ".sql.gz" if compress else ".sql"
|
||||
filename = f"postgres_{database}_{date_str}{ext}"
|
||||
backup_path = self.backup_dir / filename
|
||||
|
||||
try:
|
||||
cmd = ["pg_dump", uri]
|
||||
|
||||
if compress:
|
||||
# Use pg_dump with gzip
|
||||
with open(backup_path, "wb") as f:
|
||||
dump_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
gzip_proc = subprocess.Popen(
|
||||
["gzip"],
|
||||
stdin=dump_proc.stdout,
|
||||
stdout=f
|
||||
)
|
||||
dump_proc.stdout.close()
|
||||
gzip_proc.communicate()
|
||||
|
||||
if dump_proc.returncode != 0:
|
||||
print("Error: pg_dump failed")
|
||||
return None
|
||||
else:
|
||||
with open(backup_path, "w") as f:
|
||||
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error: {result.stderr}")
|
||||
return None
|
||||
|
||||
size_bytes = backup_path.stat().st_size
|
||||
|
||||
backup_info = BackupInfo(
|
||||
filename=filename,
|
||||
database_type="postgres",
|
||||
database_name=database,
|
||||
timestamp=datetime.now(),
|
||||
size_bytes=size_bytes,
|
||||
compressed=compress
|
||||
)
|
||||
|
||||
if verify:
|
||||
backup_info.verified = self._verify_backup(backup_info)
|
||||
|
||||
self._save_metadata(backup_info)
|
||||
print(f"✓ Backup created: {filename} ({self._format_size(size_bytes)})")
|
||||
|
||||
return backup_info
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating PostgreSQL backup: {e}")
|
||||
return None
|
||||
|
||||
def restore_backup(self, filename: str, uri: str, dry_run: bool = False) -> bool:
|
||||
"""
|
||||
Restore database from backup.
|
||||
|
||||
Args:
|
||||
filename: Backup filename
|
||||
uri: Database connection string
|
||||
dry_run: If True, only show what would be done
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
backup_path = self.backup_dir / filename
|
||||
|
||||
if not backup_path.exists():
|
||||
print(f"Error: Backup not found: {filename}")
|
||||
return False
|
||||
|
||||
# Load metadata
|
||||
metadata_path = backup_path.with_suffix(".json")
|
||||
if metadata_path.exists():
|
||||
with open(metadata_path) as f:
|
||||
metadata = json.load(f)
|
||||
print(f"Restoring backup from {metadata['timestamp']}")
|
||||
print(f"Database: {metadata['database_name']}")
|
||||
|
||||
if dry_run:
|
||||
print(f"Would restore from: {backup_path}")
|
||||
return True
|
||||
|
||||
print(f"Restoring backup: {filename}")
|
||||
|
||||
try:
|
||||
if self.db_type == "mongodb":
|
||||
return self._restore_mongodb(backup_path, uri)
|
||||
elif self.db_type == "postgres":
|
||||
return self._restore_postgres(backup_path, uri)
|
||||
else:
|
||||
print(f"Error: Unsupported database type: {self.db_type}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error restoring backup: {e}")
|
||||
return False
|
||||
|
||||
def _restore_mongodb(self, backup_path: Path, uri: str) -> bool:
|
||||
"""Restore MongoDB backup using mongorestore."""
|
||||
try:
|
||||
# Extract if compressed
|
||||
restore_path = backup_path
|
||||
if backup_path.suffix == ".gz":
|
||||
print("Extracting backup...")
|
||||
extract_path = backup_path.with_suffix("")
|
||||
shutil.unpack_archive(backup_path, extract_path)
|
||||
restore_path = extract_path
|
||||
|
||||
cmd = ["mongorestore", "--uri", uri, str(restore_path)]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
# Cleanup extracted files
|
||||
if restore_path != backup_path and restore_path.is_dir():
|
||||
shutil.rmtree(restore_path)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error: {result.stderr}")
|
||||
return False
|
||||
|
||||
print("✓ Restore completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error restoring MongoDB: {e}")
|
||||
return False
|
||||
|
||||
def _restore_postgres(self, backup_path: Path, uri: str) -> bool:
|
||||
"""Restore PostgreSQL backup using psql."""
|
||||
try:
|
||||
if backup_path.suffix == ".gz":
|
||||
# Decompress and restore
|
||||
with gzip.open(backup_path, "rb") as f:
|
||||
cmd = ["psql", uri]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdin=f,
|
||||
capture_output=True,
|
||||
text=False
|
||||
)
|
||||
else:
|
||||
with open(backup_path) as f:
|
||||
cmd = ["psql", uri]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdin=f,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error: {result.stderr}")
|
||||
return False
|
||||
|
||||
print("✓ Restore completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error restoring PostgreSQL: {e}")
|
||||
return False
|
||||
|
||||
def list_backups(self) -> List[BackupInfo]:
|
||||
"""
|
||||
List all backups.
|
||||
|
||||
Returns:
|
||||
List of BackupInfo objects
|
||||
"""
|
||||
backups = []
|
||||
|
||||
for metadata_file in sorted(self.backup_dir.glob("*.json")):
|
||||
try:
|
||||
with open(metadata_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
backup_info = BackupInfo(
|
||||
filename=data["filename"],
|
||||
database_type=data["database_type"],
|
||||
database_name=data["database_name"],
|
||||
timestamp=datetime.fromisoformat(data["timestamp"]),
|
||||
size_bytes=data["size_bytes"],
|
||||
compressed=data["compressed"],
|
||||
verified=data.get("verified", False)
|
||||
)
|
||||
backups.append(backup_info)
|
||||
except Exception as e:
|
||||
print(f"Error reading metadata {metadata_file}: {e}")
|
||||
|
||||
return backups
|
||||
|
||||
def cleanup_old_backups(self, retention_days: int, dry_run: bool = False) -> int:
|
||||
"""
|
||||
Remove backups older than retention period.
|
||||
|
||||
Args:
|
||||
retention_days: Number of days to retain backups
|
||||
dry_run: If True, only show what would be deleted
|
||||
|
||||
Returns:
|
||||
Number of backups removed
|
||||
"""
|
||||
cutoff = datetime.now().timestamp() - (retention_days * 24 * 3600)
|
||||
removed = 0
|
||||
|
||||
for backup_file in self.backup_dir.glob("*"):
|
||||
if backup_file.suffix == ".json":
|
||||
continue
|
||||
|
||||
if backup_file.stat().st_mtime < cutoff:
|
||||
if dry_run:
|
||||
print(f"Would remove: {backup_file.name}")
|
||||
else:
|
||||
print(f"Removing: {backup_file.name}")
|
||||
backup_file.unlink()
|
||||
# Remove metadata
|
||||
metadata_file = backup_file.with_suffix(".json")
|
||||
if metadata_file.exists():
|
||||
metadata_file.unlink()
|
||||
removed += 1
|
||||
|
||||
return removed
|
||||
|
||||
def _verify_backup(self, backup_info: BackupInfo) -> bool:
|
||||
"""
|
||||
Verify backup integrity.
|
||||
|
||||
Args:
|
||||
backup_info: Backup information
|
||||
|
||||
Returns:
|
||||
True if backup is valid, False otherwise
|
||||
"""
|
||||
backup_path = self.backup_dir / backup_info.filename
|
||||
|
||||
if not backup_path.exists():
|
||||
return False
|
||||
|
||||
# Basic verification: file exists and has size > 0
|
||||
if backup_path.stat().st_size == 0:
|
||||
return False
|
||||
|
||||
# Could add more verification here (checksums, test restore, etc.)
|
||||
return True
|
||||
|
||||
def _get_size(self, path: Path) -> int:
|
||||
"""Get total size of file or directory."""
|
||||
if path.is_file():
|
||||
return path.stat().st_size
|
||||
elif path.is_dir():
|
||||
total = 0
|
||||
for item in path.rglob("*"):
|
||||
if item.is_file():
|
||||
total += item.stat().st_size
|
||||
return total
|
||||
return 0
|
||||
|
||||
def _format_size(self, size_bytes: int) -> str:
|
||||
"""Format size in human-readable format."""
|
||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes:.2f} {unit}"
|
||||
size_bytes /= 1024
|
||||
return f"{size_bytes:.2f} PB"
|
||||
|
||||
def _save_metadata(self, backup_info: BackupInfo):
|
||||
"""Save backup metadata to JSON file."""
|
||||
metadata_path = self.backup_dir / f"{backup_info.filename}.json"
|
||||
|
||||
metadata = {
|
||||
"filename": backup_info.filename,
|
||||
"database_type": backup_info.database_type,
|
||||
"database_name": backup_info.database_name,
|
||||
"timestamp": backup_info.timestamp.isoformat(),
|
||||
"size_bytes": backup_info.size_bytes,
|
||||
"compressed": backup_info.compressed,
|
||||
"verified": backup_info.verified
|
||||
}
|
||||
|
||||
with open(metadata_path, "w") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Database backup tool")
|
||||
parser.add_argument("--db", required=True, choices=["mongodb", "postgres"],
|
||||
help="Database type")
|
||||
parser.add_argument("--backup-dir", default="./backups",
|
||||
help="Backup directory")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# Backup command
|
||||
backup_parser = subparsers.add_parser("backup", help="Create backup")
|
||||
backup_parser.add_argument("--uri", required=True, help="Database connection string")
|
||||
backup_parser.add_argument("--database", help="Database name")
|
||||
backup_parser.add_argument("--no-compress", action="store_true",
|
||||
help="Disable compression")
|
||||
backup_parser.add_argument("--no-verify", action="store_true",
|
||||
help="Skip verification")
|
||||
|
||||
# Restore command
|
||||
restore_parser = subparsers.add_parser("restore", help="Restore backup")
|
||||
restore_parser.add_argument("filename", help="Backup filename")
|
||||
restore_parser.add_argument("--uri", required=True, help="Database connection string")
|
||||
restore_parser.add_argument("--dry-run", action="store_true",
|
||||
help="Show what would be done")
|
||||
|
||||
# List command
|
||||
subparsers.add_parser("list", help="List backups")
|
||||
|
||||
# Cleanup command
|
||||
cleanup_parser = subparsers.add_parser("cleanup", help="Remove old backups")
|
||||
cleanup_parser.add_argument("--retention-days", type=int, default=7,
|
||||
help="Days to retain backups (default: 7)")
|
||||
cleanup_parser.add_argument("--dry-run", action="store_true",
|
||||
help="Show what would be removed")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
manager = BackupManager(args.db, args.backup_dir)
|
||||
|
||||
if args.command == "backup":
|
||||
backup_info = manager.create_backup(
|
||||
args.uri,
|
||||
args.database,
|
||||
compress=not args.no_compress,
|
||||
verify=not args.no_verify
|
||||
)
|
||||
sys.exit(0 if backup_info else 1)
|
||||
|
||||
elif args.command == "restore":
|
||||
success = manager.restore_backup(args.filename, args.uri, args.dry_run)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.command == "list":
|
||||
backups = manager.list_backups()
|
||||
print(f"Total backups: {len(backups)}\n")
|
||||
for backup in backups:
|
||||
verified_str = "✓" if backup.verified else "?"
|
||||
print(f"[{verified_str}] {backup.filename}")
|
||||
print(f" Database: {backup.database_name}")
|
||||
print(f" Created: {backup.timestamp}")
|
||||
print(f" Size: {manager._format_size(backup.size_bytes)}")
|
||||
print()
|
||||
|
||||
elif args.command == "cleanup":
|
||||
removed = manager.cleanup_old_backups(args.retention_days, args.dry_run)
|
||||
print(f"Removed {removed} backup(s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
414
skills/databases/scripts/db_migrate.py
Normal file
414
skills/databases/scripts/db_migrate.py
Normal file
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database migration tool for MongoDB and PostgreSQL.
|
||||
Generates and applies schema migrations with rollback support.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
try:
|
||||
from pymongo import MongoClient
|
||||
MONGO_AVAILABLE = True
|
||||
except ImportError:
|
||||
MONGO_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2 import sql
|
||||
POSTGRES_AVAILABLE = True
|
||||
except ImportError:
|
||||
POSTGRES_AVAILABLE = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Migration:
|
||||
"""Represents a database migration."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
timestamp: datetime
|
||||
database_type: str
|
||||
up_sql: Optional[str] = None
|
||||
down_sql: Optional[str] = None
|
||||
mongodb_operations: Optional[List[Dict[str, Any]]] = None
|
||||
applied: bool = False
|
||||
|
||||
|
||||
class MigrationManager:
|
||||
"""Manages database migrations for MongoDB and PostgreSQL."""
|
||||
|
||||
def __init__(self, db_type: str, connection_string: str, migrations_dir: str = "./migrations"):
|
||||
"""
|
||||
Initialize migration manager.
|
||||
|
||||
Args:
|
||||
db_type: Database type ('mongodb' or 'postgres')
|
||||
connection_string: Database connection string
|
||||
migrations_dir: Directory to store migration files
|
||||
"""
|
||||
self.db_type = db_type.lower()
|
||||
self.connection_string = connection_string
|
||||
self.migrations_dir = Path(migrations_dir)
|
||||
self.migrations_dir.mkdir(exist_ok=True)
|
||||
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.conn = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Connect to database.
|
||||
|
||||
Returns:
|
||||
True if connection successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if self.db_type == "mongodb":
|
||||
if not MONGO_AVAILABLE:
|
||||
print("Error: pymongo not installed")
|
||||
return False
|
||||
self.client = MongoClient(self.connection_string)
|
||||
self.db = self.client.get_default_database()
|
||||
# Test connection
|
||||
self.client.server_info()
|
||||
return True
|
||||
|
||||
elif self.db_type == "postgres":
|
||||
if not POSTGRES_AVAILABLE:
|
||||
print("Error: psycopg2 not installed")
|
||||
return False
|
||||
self.conn = psycopg2.connect(self.connection_string)
|
||||
return True
|
||||
|
||||
else:
|
||||
print(f"Error: Unsupported database type: {self.db_type}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Connection error: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from database."""
|
||||
try:
|
||||
if self.client:
|
||||
self.client.close()
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
except Exception as e:
|
||||
print(f"Disconnect error: {e}")
|
||||
|
||||
def _ensure_migrations_table(self):
|
||||
"""Create migrations tracking table/collection if not exists."""
|
||||
if self.db_type == "mongodb":
|
||||
# MongoDB creates collection automatically
|
||||
pass
|
||||
elif self.db_type == "postgres":
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
def generate_migration(self, name: str, dry_run: bool = False) -> Optional[Migration]:
|
||||
"""
|
||||
Generate new migration file.
|
||||
|
||||
Args:
|
||||
name: Migration name
|
||||
dry_run: If True, only show what would be generated
|
||||
|
||||
Returns:
|
||||
Migration object if successful, None otherwise
|
||||
"""
|
||||
timestamp = datetime.now()
|
||||
migration_id = timestamp.strftime("%Y%m%d%H%M%S")
|
||||
filename = f"{migration_id}_{name}.json"
|
||||
filepath = self.migrations_dir / filename
|
||||
|
||||
migration = Migration(
|
||||
id=migration_id,
|
||||
name=name,
|
||||
timestamp=timestamp,
|
||||
database_type=self.db_type
|
||||
)
|
||||
|
||||
if self.db_type == "mongodb":
|
||||
migration.mongodb_operations = [
|
||||
{
|
||||
"operation": "createIndex",
|
||||
"collection": "example_collection",
|
||||
"index": {"field": 1},
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
elif self.db_type == "postgres":
|
||||
migration.up_sql = "-- Add your SQL here\n"
|
||||
migration.down_sql = "-- Add rollback SQL here\n"
|
||||
|
||||
migration_data = {
|
||||
"id": migration.id,
|
||||
"name": migration.name,
|
||||
"timestamp": migration.timestamp.isoformat(),
|
||||
"database_type": migration.database_type,
|
||||
"up_sql": migration.up_sql,
|
||||
"down_sql": migration.down_sql,
|
||||
"mongodb_operations": migration.mongodb_operations
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
print(f"Would create: {filepath}")
|
||||
print(json.dumps(migration_data, indent=2))
|
||||
return migration
|
||||
|
||||
try:
|
||||
with open(filepath, "w") as f:
|
||||
json.dump(migration_data, f, indent=2)
|
||||
print(f"Created migration: {filepath}")
|
||||
return migration
|
||||
except Exception as e:
|
||||
print(f"Error creating migration: {e}")
|
||||
return None
|
||||
|
||||
def get_pending_migrations(self) -> List[Migration]:
|
||||
"""
|
||||
Get list of pending migrations.
|
||||
|
||||
Returns:
|
||||
List of pending Migration objects
|
||||
"""
|
||||
# Get applied migrations
|
||||
applied_ids = set()
|
||||
|
||||
try:
|
||||
if self.db_type == "mongodb":
|
||||
applied_ids = {
|
||||
doc["id"] for doc in self.db.migrations.find({}, {"id": 1})
|
||||
}
|
||||
elif self.db_type == "postgres":
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute("SELECT id FROM migrations")
|
||||
applied_ids = {row[0] for row in cur.fetchall()}
|
||||
except Exception as e:
|
||||
print(f"Error reading applied migrations: {e}")
|
||||
|
||||
# Get all migration files
|
||||
pending = []
|
||||
for filepath in sorted(self.migrations_dir.glob("*.json")):
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
data = json.load(f)
|
||||
|
||||
if data["id"] not in applied_ids:
|
||||
migration = Migration(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
timestamp=datetime.fromisoformat(data["timestamp"]),
|
||||
database_type=data["database_type"],
|
||||
up_sql=data.get("up_sql"),
|
||||
down_sql=data.get("down_sql"),
|
||||
mongodb_operations=data.get("mongodb_operations")
|
||||
)
|
||||
pending.append(migration)
|
||||
except Exception as e:
|
||||
print(f"Error reading {filepath}: {e}")
|
||||
|
||||
return pending
|
||||
|
||||
def apply_migration(self, migration: Migration, dry_run: bool = False) -> bool:
|
||||
"""
|
||||
Apply migration.
|
||||
|
||||
Args:
|
||||
migration: Migration to apply
|
||||
dry_run: If True, only show what would be executed
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
print(f"Applying migration: {migration.id} - {migration.name}")
|
||||
|
||||
if dry_run:
|
||||
if self.db_type == "mongodb":
|
||||
print("MongoDB operations:")
|
||||
print(json.dumps(migration.mongodb_operations, indent=2))
|
||||
elif self.db_type == "postgres":
|
||||
print("SQL to execute:")
|
||||
print(migration.up_sql)
|
||||
return True
|
||||
|
||||
try:
|
||||
if self.db_type == "mongodb":
|
||||
for op in migration.mongodb_operations or []:
|
||||
if op["operation"] == "createIndex":
|
||||
self.db[op["collection"]].create_index(
|
||||
list(op["index"].items()),
|
||||
**op.get("options", {})
|
||||
)
|
||||
|
||||
# Record migration
|
||||
self.db.migrations.insert_one({
|
||||
"id": migration.id,
|
||||
"name": migration.name,
|
||||
"applied_at": datetime.now()
|
||||
})
|
||||
|
||||
elif self.db_type == "postgres":
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(migration.up_sql)
|
||||
|
||||
# Record migration
|
||||
cur.execute(
|
||||
"INSERT INTO migrations (id, name) VALUES (%s, %s)",
|
||||
(migration.id, migration.name)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
print(f"✓ Applied: {migration.id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error applying migration: {e}")
|
||||
if self.conn:
|
||||
self.conn.rollback()
|
||||
return False
|
||||
|
||||
def rollback_migration(self, migration_id: str, dry_run: bool = False) -> bool:
|
||||
"""
|
||||
Rollback migration.
|
||||
|
||||
Args:
|
||||
migration_id: Migration ID to rollback
|
||||
dry_run: If True, only show what would be executed
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Find migration file
|
||||
migration_file = None
|
||||
for filepath in self.migrations_dir.glob(f"{migration_id}_*.json"):
|
||||
migration_file = filepath
|
||||
break
|
||||
|
||||
if not migration_file:
|
||||
print(f"Migration not found: {migration_id}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(migration_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"Rolling back: {migration_id} - {data['name']}")
|
||||
|
||||
if dry_run:
|
||||
if self.db_type == "postgres":
|
||||
print("SQL to execute:")
|
||||
print(data.get("down_sql", "-- No rollback defined"))
|
||||
return True
|
||||
|
||||
if self.db_type == "postgres" and data.get("down_sql"):
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(data["down_sql"])
|
||||
cur.execute("DELETE FROM migrations WHERE id = %s", (migration_id,))
|
||||
self.conn.commit()
|
||||
elif self.db_type == "mongodb":
|
||||
self.db.migrations.delete_one({"id": migration_id})
|
||||
|
||||
print(f"✓ Rolled back: {migration_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error rolling back: {e}")
|
||||
if self.conn:
|
||||
self.conn.rollback()
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Database migration tool")
|
||||
parser.add_argument("--db", required=True, choices=["mongodb", "postgres"],
|
||||
help="Database type")
|
||||
parser.add_argument("--uri", help="Database connection string")
|
||||
parser.add_argument("--migrations-dir", default="./migrations",
|
||||
help="Migrations directory")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# Generate command
|
||||
gen_parser = subparsers.add_parser("generate", help="Generate new migration")
|
||||
gen_parser.add_argument("name", help="Migration name")
|
||||
gen_parser.add_argument("--dry-run", action="store_true",
|
||||
help="Show what would be generated")
|
||||
|
||||
# Apply command
|
||||
apply_parser = subparsers.add_parser("apply", help="Apply pending migrations")
|
||||
apply_parser.add_argument("--dry-run", action="store_true",
|
||||
help="Show what would be executed")
|
||||
|
||||
# Rollback command
|
||||
rollback_parser = subparsers.add_parser("rollback", help="Rollback migration")
|
||||
rollback_parser.add_argument("id", help="Migration ID to rollback")
|
||||
rollback_parser.add_argument("--dry-run", action="store_true",
|
||||
help="Show what would be executed")
|
||||
|
||||
# Status command
|
||||
subparsers.add_parser("status", help="Show migration status")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# For generate, we don't need connection
|
||||
if args.command == "generate":
|
||||
manager = MigrationManager(args.db, "", args.migrations_dir)
|
||||
migration = manager.generate_migration(args.name, args.dry_run)
|
||||
sys.exit(0 if migration else 1)
|
||||
|
||||
# Other commands need connection
|
||||
if not args.uri:
|
||||
print("Error: --uri required for this command")
|
||||
sys.exit(1)
|
||||
|
||||
manager = MigrationManager(args.db, args.uri, args.migrations_dir)
|
||||
|
||||
if not manager.connect():
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
manager._ensure_migrations_table()
|
||||
|
||||
if args.command == "status":
|
||||
pending = manager.get_pending_migrations()
|
||||
print(f"Pending migrations: {len(pending)}")
|
||||
for migration in pending:
|
||||
print(f" {migration.id} - {migration.name}")
|
||||
|
||||
elif args.command == "apply":
|
||||
pending = manager.get_pending_migrations()
|
||||
if not pending:
|
||||
print("No pending migrations")
|
||||
else:
|
||||
for migration in pending:
|
||||
if not manager.apply_migration(migration, args.dry_run):
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "rollback":
|
||||
if not manager.rollback_migration(args.id, args.dry_run):
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
manager.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
444
skills/databases/scripts/db_performance_check.py
Normal file
444
skills/databases/scripts/db_performance_check.py
Normal file
@@ -0,0 +1,444 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database performance analysis tool for MongoDB and PostgreSQL.
|
||||
Analyzes slow queries, recommends indexes, and generates reports.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
try:
|
||||
from pymongo import MongoClient
|
||||
MONGO_AVAILABLE = True
|
||||
except ImportError:
|
||||
MONGO_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
POSTGRES_AVAILABLE = True
|
||||
except ImportError:
|
||||
POSTGRES_AVAILABLE = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class SlowQuery:
|
||||
"""Represents a slow query."""
|
||||
|
||||
query: str
|
||||
execution_time_ms: float
|
||||
count: int
|
||||
collection_or_table: Optional[str] = None
|
||||
index_used: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndexRecommendation:
|
||||
"""Index recommendation."""
|
||||
|
||||
collection_or_table: str
|
||||
fields: List[str]
|
||||
reason: str
|
||||
estimated_benefit: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceReport:
|
||||
"""Performance analysis report."""
|
||||
|
||||
database_type: str
|
||||
database_name: str
|
||||
timestamp: datetime
|
||||
slow_queries: List[SlowQuery]
|
||||
index_recommendations: List[IndexRecommendation]
|
||||
database_metrics: Dict[str, any]
|
||||
|
||||
|
||||
class PerformanceAnalyzer:
|
||||
"""Analyzes database performance."""
|
||||
|
||||
def __init__(self, db_type: str, connection_string: str, threshold_ms: int = 100):
|
||||
"""
|
||||
Initialize performance analyzer.
|
||||
|
||||
Args:
|
||||
db_type: Database type ('mongodb' or 'postgres')
|
||||
connection_string: Database connection string
|
||||
threshold_ms: Slow query threshold in milliseconds
|
||||
"""
|
||||
self.db_type = db_type.lower()
|
||||
self.connection_string = connection_string
|
||||
self.threshold_ms = threshold_ms
|
||||
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.conn = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to database."""
|
||||
try:
|
||||
if self.db_type == "mongodb":
|
||||
if not MONGO_AVAILABLE:
|
||||
print("Error: pymongo not installed")
|
||||
return False
|
||||
self.client = MongoClient(self.connection_string)
|
||||
self.db = self.client.get_default_database()
|
||||
self.client.server_info()
|
||||
return True
|
||||
|
||||
elif self.db_type == "postgres":
|
||||
if not POSTGRES_AVAILABLE:
|
||||
print("Error: psycopg2 not installed")
|
||||
return False
|
||||
self.conn = psycopg2.connect(self.connection_string)
|
||||
return True
|
||||
|
||||
else:
|
||||
print(f"Error: Unsupported database type: {self.db_type}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Connection error: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from database."""
|
||||
try:
|
||||
if self.client:
|
||||
self.client.close()
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
except Exception as e:
|
||||
print(f"Disconnect error: {e}")
|
||||
|
||||
def analyze(self) -> Optional[PerformanceReport]:
|
||||
"""
|
||||
Analyze database performance.
|
||||
|
||||
Returns:
|
||||
PerformanceReport if successful, None otherwise
|
||||
"""
|
||||
try:
|
||||
if self.db_type == "mongodb":
|
||||
return self._analyze_mongodb()
|
||||
elif self.db_type == "postgres":
|
||||
return self._analyze_postgres()
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Analysis error: {e}")
|
||||
return None
|
||||
|
||||
def _analyze_mongodb(self) -> PerformanceReport:
|
||||
"""Analyze MongoDB performance."""
|
||||
slow_queries = []
|
||||
index_recommendations = []
|
||||
|
||||
# Enable profiling if not enabled
|
||||
profiling_level = self.db.command("profile", -1)
|
||||
if profiling_level.get("was", 0) == 0:
|
||||
self.db.command("profile", 1, slowms=self.threshold_ms)
|
||||
|
||||
# Get slow queries from system.profile
|
||||
for doc in self.db.system.profile.find(
|
||||
{"millis": {"$gte": self.threshold_ms}},
|
||||
limit=50
|
||||
).sort("millis", -1):
|
||||
|
||||
query_str = json.dumps(doc.get("command", {}), default=str)
|
||||
|
||||
slow_queries.append(SlowQuery(
|
||||
query=query_str,
|
||||
execution_time_ms=doc.get("millis", 0),
|
||||
count=1,
|
||||
collection_or_table=doc.get("ns", "").split(".")[-1] if "ns" in doc else None,
|
||||
index_used=doc.get("planSummary")
|
||||
))
|
||||
|
||||
# Analyze collections for index recommendations
|
||||
for coll_name in self.db.list_collection_names():
|
||||
if coll_name.startswith("system."):
|
||||
continue
|
||||
|
||||
coll = self.db[coll_name]
|
||||
|
||||
# Check for collections scans
|
||||
stats = coll.aggregate([
|
||||
{"$collStats": {"storageStats": {}}}
|
||||
]).next()
|
||||
|
||||
# Check if collection has indexes
|
||||
indexes = list(coll.list_indexes())
|
||||
|
||||
if len(indexes) <= 1: # Only _id index
|
||||
# Recommend indexes based on common patterns
|
||||
# Sample documents to find frequently queried fields
|
||||
sample = list(coll.find().limit(100))
|
||||
|
||||
if sample:
|
||||
# Find fields that appear in most documents
|
||||
field_freq = {}
|
||||
for doc in sample:
|
||||
for field in doc.keys():
|
||||
if field != "_id":
|
||||
field_freq[field] = field_freq.get(field, 0) + 1
|
||||
|
||||
# Recommend index on most common field
|
||||
if field_freq:
|
||||
top_field = max(field_freq.items(), key=lambda x: x[1])[0]
|
||||
index_recommendations.append(IndexRecommendation(
|
||||
collection_or_table=coll_name,
|
||||
fields=[top_field],
|
||||
reason="Frequently queried field without index",
|
||||
estimated_benefit="High"
|
||||
))
|
||||
|
||||
# Get database metrics
|
||||
server_status = self.client.admin.command("serverStatus")
|
||||
db_stats = self.db.command("dbStats")
|
||||
|
||||
metrics = {
|
||||
"connections": server_status.get("connections", {}).get("current", 0),
|
||||
"operations_per_sec": server_status.get("opcounters", {}).get("query", 0),
|
||||
"database_size_mb": db_stats.get("dataSize", 0) / (1024 * 1024),
|
||||
"index_size_mb": db_stats.get("indexSize", 0) / (1024 * 1024),
|
||||
"collections": db_stats.get("collections", 0)
|
||||
}
|
||||
|
||||
return PerformanceReport(
|
||||
database_type="mongodb",
|
||||
database_name=self.db.name,
|
||||
timestamp=datetime.now(),
|
||||
slow_queries=slow_queries[:10], # Top 10
|
||||
index_recommendations=index_recommendations,
|
||||
database_metrics=metrics
|
||||
)
|
||||
|
||||
def _analyze_postgres(self) -> PerformanceReport:
|
||||
"""Analyze PostgreSQL performance."""
|
||||
slow_queries = []
|
||||
index_recommendations = []
|
||||
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# Check if pg_stat_statements extension is available
|
||||
cur.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
||||
) AS has_extension
|
||||
""")
|
||||
has_pg_stat_statements = cur.fetchone()["has_extension"]
|
||||
|
||||
if has_pg_stat_statements:
|
||||
# Get slow queries from pg_stat_statements
|
||||
cur.execute("""
|
||||
SELECT
|
||||
query,
|
||||
mean_exec_time,
|
||||
calls,
|
||||
total_exec_time
|
||||
FROM pg_stat_statements
|
||||
WHERE mean_exec_time >= %s
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 10
|
||||
""", (self.threshold_ms,))
|
||||
|
||||
for row in cur.fetchall():
|
||||
slow_queries.append(SlowQuery(
|
||||
query=row["query"],
|
||||
execution_time_ms=row["mean_exec_time"],
|
||||
count=row["calls"]
|
||||
))
|
||||
|
||||
# Find tables with sequential scans (potential index candidates)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
seq_scan,
|
||||
seq_tup_read,
|
||||
idx_scan
|
||||
FROM pg_stat_user_tables
|
||||
WHERE seq_scan > 1000
|
||||
AND (idx_scan IS NULL OR seq_scan > idx_scan * 2)
|
||||
ORDER BY seq_tup_read DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
for row in cur.fetchall():
|
||||
index_recommendations.append(IndexRecommendation(
|
||||
collection_or_table=f"{row['schemaname']}.{row['tablename']}",
|
||||
fields=["<analyze query patterns>"],
|
||||
reason=f"High sequential scans ({row['seq_scan']}) vs index scans ({row['idx_scan'] or 0})",
|
||||
estimated_benefit="High" if row["seq_tup_read"] > 100000 else "Medium"
|
||||
))
|
||||
|
||||
# Find unused indexes
|
||||
cur.execute("""
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
idx_scan
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE idx_scan = 0
|
||||
AND indexname NOT LIKE '%_pkey'
|
||||
ORDER BY pg_relation_size(indexrelid) DESC
|
||||
""")
|
||||
|
||||
unused_indexes = []
|
||||
for row in cur.fetchall():
|
||||
unused_indexes.append(
|
||||
f"{row['schemaname']}.{row['tablename']}.{row['indexname']}"
|
||||
)
|
||||
|
||||
# Database metrics
|
||||
cur.execute("""
|
||||
SELECT
|
||||
sum(numbackends) AS connections,
|
||||
sum(xact_commit) AS commits,
|
||||
sum(xact_rollback) AS rollbacks
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database()
|
||||
""")
|
||||
stats = cur.fetchone()
|
||||
|
||||
cur.execute("""
|
||||
SELECT pg_database_size(current_database()) AS db_size
|
||||
""")
|
||||
db_size = cur.fetchone()["db_size"]
|
||||
|
||||
cur.execute("""
|
||||
SELECT
|
||||
sum(heap_blks_hit) / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0) AS cache_hit_ratio
|
||||
FROM pg_statio_user_tables
|
||||
""")
|
||||
cache_ratio = cur.fetchone()["cache_hit_ratio"] or 0
|
||||
|
||||
metrics = {
|
||||
"connections": stats["connections"],
|
||||
"commits": stats["commits"],
|
||||
"rollbacks": stats["rollbacks"],
|
||||
"database_size_mb": db_size / (1024 * 1024),
|
||||
"cache_hit_ratio": float(cache_ratio),
|
||||
"unused_indexes": unused_indexes
|
||||
}
|
||||
|
||||
return PerformanceReport(
|
||||
database_type="postgres",
|
||||
database_name=self.conn.info.dbname,
|
||||
timestamp=datetime.now(),
|
||||
slow_queries=slow_queries,
|
||||
index_recommendations=index_recommendations,
|
||||
database_metrics=metrics
|
||||
)
|
||||
|
||||
def print_report(self, report: PerformanceReport):
|
||||
"""Print performance report."""
|
||||
print("=" * 80)
|
||||
print(f"Database Performance Report - {report.database_type.upper()}")
|
||||
print(f"Database: {report.database_name}")
|
||||
print(f"Timestamp: {report.timestamp}")
|
||||
print("=" * 80)
|
||||
|
||||
print("\n## Database Metrics")
|
||||
print("-" * 80)
|
||||
for key, value in report.database_metrics.items():
|
||||
if isinstance(value, float):
|
||||
print(f"{key}: {value:.2f}")
|
||||
else:
|
||||
print(f"{key}: {value}")
|
||||
|
||||
print("\n## Slow Queries")
|
||||
print("-" * 80)
|
||||
if report.slow_queries:
|
||||
for i, query in enumerate(report.slow_queries, 1):
|
||||
print(f"\n{i}. Execution Time: {query.execution_time_ms:.2f}ms | Count: {query.count}")
|
||||
if query.collection_or_table:
|
||||
print(f" Collection/Table: {query.collection_or_table}")
|
||||
if query.index_used:
|
||||
print(f" Index Used: {query.index_used}")
|
||||
print(f" Query: {query.query[:200]}...")
|
||||
else:
|
||||
print("No slow queries found")
|
||||
|
||||
print("\n## Index Recommendations")
|
||||
print("-" * 80)
|
||||
if report.index_recommendations:
|
||||
for i, rec in enumerate(report.index_recommendations, 1):
|
||||
print(f"\n{i}. {rec.collection_or_table}")
|
||||
print(f" Fields: {', '.join(rec.fields)}")
|
||||
print(f" Reason: {rec.reason}")
|
||||
print(f" Estimated Benefit: {rec.estimated_benefit}")
|
||||
|
||||
if report.database_type == "mongodb":
|
||||
index_spec = {field: 1 for field in rec.fields}
|
||||
print(f" Command: db.{rec.collection_or_table}.createIndex({json.dumps(index_spec)})")
|
||||
elif report.database_type == "postgres":
|
||||
fields_str = ", ".join(rec.fields)
|
||||
print(f" Command: CREATE INDEX idx_{rec.collection_or_table.replace('.', '_')}_{rec.fields[0]} ON {rec.collection_or_table}({fields_str});")
|
||||
else:
|
||||
print("No index recommendations")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
|
||||
def save_report(self, report: PerformanceReport, filename: str):
|
||||
"""Save report to JSON file."""
|
||||
# Convert dataclasses to dict
|
||||
report_dict = {
|
||||
"database_type": report.database_type,
|
||||
"database_name": report.database_name,
|
||||
"timestamp": report.timestamp.isoformat(),
|
||||
"slow_queries": [asdict(q) for q in report.slow_queries],
|
||||
"index_recommendations": [asdict(r) for r in report.index_recommendations],
|
||||
"database_metrics": report.database_metrics
|
||||
}
|
||||
|
||||
with open(filename, "w") as f:
|
||||
json.dump(report_dict, f, indent=2, default=str)
|
||||
|
||||
print(f"\nReport saved to: {filename}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Database performance analysis tool")
|
||||
parser.add_argument("--db", required=True, choices=["mongodb", "postgres"],
|
||||
help="Database type")
|
||||
parser.add_argument("--uri", required=True, help="Database connection string")
|
||||
parser.add_argument("--threshold", type=int, default=100,
|
||||
help="Slow query threshold in milliseconds (default: 100)")
|
||||
parser.add_argument("--output", help="Save report to JSON file")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
analyzer = PerformanceAnalyzer(args.db, args.uri, args.threshold)
|
||||
|
||||
if not analyzer.connect():
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
print(f"Analyzing {args.db} performance (threshold: {args.threshold}ms)...")
|
||||
report = analyzer.analyze()
|
||||
|
||||
if report:
|
||||
analyzer.print_report(report)
|
||||
|
||||
if args.output:
|
||||
analyzer.save_report(report, args.output)
|
||||
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Analysis failed")
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
analyzer.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
20
skills/databases/scripts/requirements.txt
Normal file
20
skills/databases/scripts/requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
# Databases Skill Dependencies
|
||||
# Python 3.10+ required
|
||||
|
||||
# No Python package dependencies - uses only standard library
|
||||
|
||||
# Testing dependencies (dev)
|
||||
pytest>=8.0.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-mock>=3.12.0
|
||||
|
||||
# Note: This skill requires database CLI tools:
|
||||
#
|
||||
# PostgreSQL:
|
||||
# - psql CLI (comes with PostgreSQL)
|
||||
# - Ubuntu/Debian: sudo apt-get install postgresql-client
|
||||
# - macOS: brew install postgresql
|
||||
#
|
||||
# MongoDB:
|
||||
# - mongosh CLI: https://www.mongodb.com/try/download/shell
|
||||
# - mongodump/mongorestore: https://www.mongodb.com/try/download/database-tools
|
||||
1
skills/databases/scripts/tests/coverage-db.json
Normal file
1
skills/databases/scripts/tests/coverage-db.json
Normal file
File diff suppressed because one or more lines are too long
4
skills/databases/scripts/tests/requirements.txt
Normal file
4
skills/databases/scripts/tests/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
pytest-mock>=3.10.0
|
||||
mongomock>=4.1.0
|
||||
340
skills/databases/scripts/tests/test_db_backup.py
Normal file
340
skills/databases/scripts/tests/test_db_backup.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""Tests for db_backup.py"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock, call
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from db_backup import BackupInfo, BackupManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_backup_dir(tmp_path):
|
||||
"""Create temporary backup directory."""
|
||||
backup_dir = tmp_path / "backups"
|
||||
backup_dir.mkdir()
|
||||
return str(backup_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_backup_info():
|
||||
"""Create sample backup info."""
|
||||
return BackupInfo(
|
||||
filename="test_backup_20250101_120000.dump",
|
||||
database_type="mongodb",
|
||||
database_name="testdb",
|
||||
timestamp=datetime.now(),
|
||||
size_bytes=1024000,
|
||||
compressed=True,
|
||||
verified=True
|
||||
)
|
||||
|
||||
|
||||
class TestBackupInfo:
|
||||
"""Test BackupInfo dataclass."""
|
||||
|
||||
def test_backup_info_creation(self):
|
||||
"""Test creating backup info object."""
|
||||
info = BackupInfo(
|
||||
filename="backup.dump",
|
||||
database_type="mongodb",
|
||||
database_name="mydb",
|
||||
timestamp=datetime.now(),
|
||||
size_bytes=1024,
|
||||
compressed=False
|
||||
)
|
||||
|
||||
assert info.filename == "backup.dump"
|
||||
assert info.database_type == "mongodb"
|
||||
assert info.database_name == "mydb"
|
||||
assert info.size_bytes == 1024
|
||||
assert not info.compressed
|
||||
assert not info.verified
|
||||
|
||||
|
||||
class TestBackupManager:
|
||||
"""Test BackupManager class."""
|
||||
|
||||
def test_init(self, temp_backup_dir):
|
||||
"""Test manager initialization."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
assert manager.db_type == "mongodb"
|
||||
assert Path(temp_backup_dir).exists()
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_backup_mongodb(self, mock_run, temp_backup_dir):
|
||||
"""Test MongoDB backup creation."""
|
||||
mock_run.return_value = Mock(returncode=0, stderr="")
|
||||
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
backup_info = manager.create_backup(
|
||||
"mongodb://localhost",
|
||||
"testdb",
|
||||
compress=False,
|
||||
verify=False
|
||||
)
|
||||
|
||||
assert backup_info is not None
|
||||
assert backup_info.database_type == "mongodb"
|
||||
assert backup_info.database_name == "testdb"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_backup_postgres(self, mock_run, temp_backup_dir):
|
||||
"""Test PostgreSQL backup creation."""
|
||||
mock_run.return_value = Mock(returncode=0, stderr="")
|
||||
|
||||
manager = BackupManager("postgres", temp_backup_dir)
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
mock_open.return_value.__enter__.return_value = MagicMock()
|
||||
|
||||
backup_info = manager.create_backup(
|
||||
"postgresql://localhost/testdb",
|
||||
"testdb",
|
||||
compress=False,
|
||||
verify=False
|
||||
)
|
||||
|
||||
assert backup_info is not None
|
||||
assert backup_info.database_type == "postgres"
|
||||
assert backup_info.database_name == "testdb"
|
||||
|
||||
def test_backup_postgres_no_database(self, temp_backup_dir):
|
||||
"""Test PostgreSQL backup without database name."""
|
||||
manager = BackupManager("postgres", temp_backup_dir)
|
||||
backup_info = manager.create_backup(
|
||||
"postgresql://localhost",
|
||||
database=None,
|
||||
compress=False,
|
||||
verify=False
|
||||
)
|
||||
|
||||
assert backup_info is None
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_backup_with_compression(self, mock_run, temp_backup_dir):
|
||||
"""Test backup with compression."""
|
||||
mock_run.return_value = Mock(returncode=0, stderr="")
|
||||
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
with patch('shutil.make_archive') as mock_archive, \
|
||||
patch('shutil.rmtree') as mock_rmtree:
|
||||
|
||||
backup_info = manager.create_backup(
|
||||
"mongodb://localhost",
|
||||
"testdb",
|
||||
compress=True,
|
||||
verify=False
|
||||
)
|
||||
|
||||
assert backup_info is not None
|
||||
assert backup_info.compressed
|
||||
mock_archive.assert_called_once()
|
||||
|
||||
def test_save_and_load_metadata(self, temp_backup_dir, sample_backup_info):
|
||||
"""Test saving and loading backup metadata."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Save metadata
|
||||
manager._save_metadata(sample_backup_info)
|
||||
|
||||
# Check file was created
|
||||
metadata_file = Path(temp_backup_dir) / f"{sample_backup_info.filename}.json"
|
||||
assert metadata_file.exists()
|
||||
|
||||
# Load metadata
|
||||
with open(metadata_file) as f:
|
||||
data = json.load(f)
|
||||
assert data["filename"] == sample_backup_info.filename
|
||||
assert data["database_type"] == "mongodb"
|
||||
assert data["database_name"] == "testdb"
|
||||
|
||||
def test_list_backups(self, temp_backup_dir, sample_backup_info):
|
||||
"""Test listing backups."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create test backup metadata
|
||||
manager._save_metadata(sample_backup_info)
|
||||
|
||||
# List backups
|
||||
backups = manager.list_backups()
|
||||
|
||||
assert len(backups) == 1
|
||||
assert backups[0].filename == sample_backup_info.filename
|
||||
assert backups[0].database_name == "testdb"
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_restore_mongodb(self, mock_run, temp_backup_dir):
|
||||
"""Test MongoDB restore."""
|
||||
mock_run.return_value = Mock(returncode=0, stderr="")
|
||||
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create dummy backup file
|
||||
backup_file = Path(temp_backup_dir) / "test_backup.dump"
|
||||
backup_file.touch()
|
||||
|
||||
result = manager.restore_backup(
|
||||
"test_backup.dump",
|
||||
"mongodb://localhost"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_restore_postgres(self, mock_run, temp_backup_dir):
|
||||
"""Test PostgreSQL restore."""
|
||||
mock_run.return_value = Mock(returncode=0, stderr="")
|
||||
|
||||
manager = BackupManager("postgres", temp_backup_dir)
|
||||
|
||||
# Create dummy backup file
|
||||
backup_file = Path(temp_backup_dir) / "test_backup.sql"
|
||||
backup_file.write_text("SELECT 1;")
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
mock_open.return_value.__enter__.return_value = MagicMock()
|
||||
|
||||
result = manager.restore_backup(
|
||||
"test_backup.sql",
|
||||
"postgresql://localhost/testdb"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_restore_nonexistent_backup(self, temp_backup_dir):
|
||||
"""Test restore with non-existent backup file."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
result = manager.restore_backup(
|
||||
"nonexistent.dump",
|
||||
"mongodb://localhost"
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_restore_dry_run(self, temp_backup_dir):
|
||||
"""Test restore in dry-run mode."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create dummy backup file
|
||||
backup_file = Path(temp_backup_dir) / "test_backup.dump"
|
||||
backup_file.touch()
|
||||
|
||||
result = manager.restore_backup(
|
||||
"test_backup.dump",
|
||||
"mongodb://localhost",
|
||||
dry_run=True
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_cleanup_old_backups(self, temp_backup_dir):
|
||||
"""Test cleaning up old backups."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create old backup file (simulate by setting mtime)
|
||||
old_backup = Path(temp_backup_dir) / "old_backup.dump"
|
||||
old_backup.touch()
|
||||
|
||||
# Set mtime to 10 days ago
|
||||
old_time = datetime.now().timestamp() - (10 * 24 * 3600)
|
||||
os.utime(old_backup, (old_time, old_time))
|
||||
|
||||
# Cleanup with 7-day retention
|
||||
removed = manager.cleanup_old_backups(retention_days=7)
|
||||
|
||||
assert removed == 1
|
||||
assert not old_backup.exists()
|
||||
|
||||
def test_cleanup_dry_run(self, temp_backup_dir):
|
||||
"""Test cleanup in dry-run mode."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create old backup file
|
||||
old_backup = Path(temp_backup_dir) / "old_backup.dump"
|
||||
old_backup.touch()
|
||||
|
||||
old_time = datetime.now().timestamp() - (10 * 24 * 3600)
|
||||
os.utime(old_backup, (old_time, old_time))
|
||||
|
||||
# Cleanup with dry-run
|
||||
removed = manager.cleanup_old_backups(retention_days=7, dry_run=True)
|
||||
|
||||
assert removed == 1
|
||||
assert old_backup.exists() # File should still exist
|
||||
|
||||
def test_verify_backup(self, temp_backup_dir, sample_backup_info):
|
||||
"""Test backup verification."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create dummy backup file
|
||||
backup_file = Path(temp_backup_dir) / sample_backup_info.filename
|
||||
backup_file.write_text("backup data")
|
||||
|
||||
result = manager._verify_backup(sample_backup_info)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_verify_empty_backup(self, temp_backup_dir, sample_backup_info):
|
||||
"""Test verification of empty backup file."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
# Create empty backup file
|
||||
backup_file = Path(temp_backup_dir) / sample_backup_info.filename
|
||||
backup_file.touch()
|
||||
|
||||
result = manager._verify_backup(sample_backup_info)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_format_size(self, temp_backup_dir):
|
||||
"""Test size formatting."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
assert manager._format_size(500) == "500.00 B"
|
||||
assert manager._format_size(1024) == "1.00 KB"
|
||||
assert manager._format_size(1024 * 1024) == "1.00 MB"
|
||||
assert manager._format_size(1024 * 1024 * 1024) == "1.00 GB"
|
||||
|
||||
def test_get_size_file(self, temp_backup_dir):
|
||||
"""Test getting size of file."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
test_file = Path(temp_backup_dir) / "test.txt"
|
||||
test_file.write_text("test data")
|
||||
|
||||
size = manager._get_size(test_file)
|
||||
|
||||
assert size > 0
|
||||
|
||||
def test_get_size_directory(self, temp_backup_dir):
|
||||
"""Test getting size of directory."""
|
||||
manager = BackupManager("mongodb", temp_backup_dir)
|
||||
|
||||
test_dir = Path(temp_backup_dir) / "test_dir"
|
||||
test_dir.mkdir()
|
||||
(test_dir / "file1.txt").write_text("data1")
|
||||
(test_dir / "file2.txt").write_text("data2")
|
||||
|
||||
size = manager._get_size(test_dir)
|
||||
|
||||
assert size > 0
|
||||
|
||||
|
||||
# Import os for cleanup test
|
||||
import os
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
277
skills/databases/scripts/tests/test_db_migrate.py
Normal file
277
skills/databases/scripts/tests/test_db_migrate.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""Tests for db_migrate.py"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from db_migrate import Migration, MigrationManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_migrations_dir(tmp_path):
|
||||
"""Create temporary migrations directory."""
|
||||
migrations_dir = tmp_path / "migrations"
|
||||
migrations_dir.mkdir()
|
||||
return str(migrations_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mongo_client():
|
||||
"""Mock MongoDB client."""
|
||||
mock_client = MagicMock()
|
||||
mock_db = MagicMock()
|
||||
mock_client.get_default_database.return_value = mock_db
|
||||
mock_client.server_info.return_value = {}
|
||||
return mock_client, mock_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_postgres_conn():
|
||||
"""Mock PostgreSQL connection."""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_conn.cursor.return_value.__enter__.return_value = mock_cursor
|
||||
return mock_conn, mock_cursor
|
||||
|
||||
|
||||
class TestMigration:
|
||||
"""Test Migration dataclass."""
|
||||
|
||||
def test_migration_creation(self):
|
||||
"""Test creating migration object."""
|
||||
migration = Migration(
|
||||
id="20250101120000",
|
||||
name="test_migration",
|
||||
timestamp=datetime.now(),
|
||||
database_type="mongodb"
|
||||
)
|
||||
|
||||
assert migration.id == "20250101120000"
|
||||
assert migration.name == "test_migration"
|
||||
assert migration.database_type == "mongodb"
|
||||
assert not migration.applied
|
||||
|
||||
|
||||
class TestMigrationManager:
|
||||
"""Test MigrationManager class."""
|
||||
|
||||
def test_init(self, temp_migrations_dir):
|
||||
"""Test manager initialization."""
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
|
||||
assert manager.db_type == "mongodb"
|
||||
assert manager.connection_string == "mongodb://localhost"
|
||||
assert Path(temp_migrations_dir).exists()
|
||||
|
||||
@patch('db_migrate.MongoClient')
|
||||
def test_connect_mongodb(self, mock_client_class, temp_migrations_dir, mock_mongo_client):
|
||||
"""Test MongoDB connection."""
|
||||
mock_client, mock_db = mock_mongo_client
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
result = manager.connect()
|
||||
|
||||
assert result is True
|
||||
assert manager.client == mock_client
|
||||
assert manager.db == mock_db
|
||||
|
||||
@patch('db_migrate.psycopg2')
|
||||
def test_connect_postgres(self, mock_psycopg2, temp_migrations_dir, mock_postgres_conn):
|
||||
"""Test PostgreSQL connection."""
|
||||
mock_conn, mock_cursor = mock_postgres_conn
|
||||
mock_psycopg2.connect.return_value = mock_conn
|
||||
|
||||
manager = MigrationManager("postgres", "postgresql://localhost", temp_migrations_dir)
|
||||
result = manager.connect()
|
||||
|
||||
assert result is True
|
||||
assert manager.conn == mock_conn
|
||||
|
||||
def test_connect_unsupported_db(self, temp_migrations_dir):
|
||||
"""Test connection with unsupported database type."""
|
||||
manager = MigrationManager("unsupported", "connection_string", temp_migrations_dir)
|
||||
result = manager.connect()
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_generate_migration(self, temp_migrations_dir):
|
||||
"""Test migration generation."""
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
migration = manager.generate_migration("test_migration")
|
||||
|
||||
assert migration is not None
|
||||
assert migration.name == "test_migration"
|
||||
|
||||
# Check file was created
|
||||
migration_files = list(Path(temp_migrations_dir).glob("*.json"))
|
||||
assert len(migration_files) == 1
|
||||
|
||||
# Check file content
|
||||
with open(migration_files[0]) as f:
|
||||
data = json.load(f)
|
||||
assert data["name"] == "test_migration"
|
||||
assert data["database_type"] == "mongodb"
|
||||
|
||||
def test_generate_migration_dry_run(self, temp_migrations_dir):
|
||||
"""Test migration generation in dry-run mode."""
|
||||
manager = MigrationManager("postgres", "postgresql://localhost", temp_migrations_dir)
|
||||
migration = manager.generate_migration("test_migration", dry_run=True)
|
||||
|
||||
assert migration is not None
|
||||
|
||||
# Check no file was created
|
||||
migration_files = list(Path(temp_migrations_dir).glob("*.json"))
|
||||
assert len(migration_files) == 0
|
||||
|
||||
def test_get_pending_migrations(self, temp_migrations_dir):
|
||||
"""Test getting pending migrations."""
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
|
||||
# Create test migration file
|
||||
migration_data = {
|
||||
"id": "20250101120000",
|
||||
"name": "test_migration",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"database_type": "mongodb",
|
||||
"mongodb_operations": []
|
||||
}
|
||||
|
||||
migration_file = Path(temp_migrations_dir) / "20250101120000_test.json"
|
||||
with open(migration_file, "w") as f:
|
||||
json.dump(migration_data, f)
|
||||
|
||||
# Mock database connection
|
||||
with patch.object(manager, 'db', MagicMock()):
|
||||
manager.db.migrations.find.return_value = []
|
||||
|
||||
pending = manager.get_pending_migrations()
|
||||
|
||||
assert len(pending) == 1
|
||||
assert pending[0].id == "20250101120000"
|
||||
assert pending[0].name == "test_migration"
|
||||
|
||||
@patch('db_migrate.MongoClient')
|
||||
def test_apply_mongodb_migration(self, mock_client_class, temp_migrations_dir, mock_mongo_client):
|
||||
"""Test applying MongoDB migration."""
|
||||
mock_client, mock_db = mock_mongo_client
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
manager.connect()
|
||||
|
||||
migration = Migration(
|
||||
id="20250101120000",
|
||||
name="test_migration",
|
||||
timestamp=datetime.now(),
|
||||
database_type="mongodb",
|
||||
mongodb_operations=[
|
||||
{
|
||||
"operation": "createIndex",
|
||||
"collection": "users",
|
||||
"index": {"email": 1},
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = manager.apply_migration(migration)
|
||||
|
||||
assert result is True
|
||||
mock_db["users"].create_index.assert_called_once()
|
||||
mock_db.migrations.insert_one.assert_called_once()
|
||||
|
||||
def test_apply_migration_dry_run(self, temp_migrations_dir):
|
||||
"""Test applying migration in dry-run mode."""
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
|
||||
migration = Migration(
|
||||
id="20250101120000",
|
||||
name="test_migration",
|
||||
timestamp=datetime.now(),
|
||||
database_type="mongodb",
|
||||
mongodb_operations=[]
|
||||
)
|
||||
|
||||
result = manager.apply_migration(migration, dry_run=True)
|
||||
|
||||
assert result is True
|
||||
|
||||
@patch('db_migrate.psycopg2')
|
||||
def test_rollback_postgres_migration(self, mock_psycopg2, temp_migrations_dir, mock_postgres_conn):
|
||||
"""Test rolling back PostgreSQL migration."""
|
||||
mock_conn, mock_cursor = mock_postgres_conn
|
||||
mock_psycopg2.connect.return_value = mock_conn
|
||||
|
||||
manager = MigrationManager("postgres", "postgresql://localhost", temp_migrations_dir)
|
||||
manager.connect()
|
||||
|
||||
# Create migration file
|
||||
migration_data = {
|
||||
"id": "20250101120000",
|
||||
"name": "test_migration",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"database_type": "postgres",
|
||||
"up_sql": "CREATE TABLE test (id INT);",
|
||||
"down_sql": "DROP TABLE test;"
|
||||
}
|
||||
|
||||
migration_file = Path(temp_migrations_dir) / "20250101120000_test.json"
|
||||
with open(migration_file, "w") as f:
|
||||
json.dump(migration_data, f)
|
||||
|
||||
result = manager.rollback_migration("20250101120000")
|
||||
|
||||
assert result is True
|
||||
# Verify SQL was executed
|
||||
assert mock_cursor.execute.call_count >= 1
|
||||
|
||||
def test_rollback_migration_not_found(self, temp_migrations_dir):
|
||||
"""Test rollback with non-existent migration."""
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
|
||||
result = manager.rollback_migration("99999999999999")
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_migration_sorting(temp_migrations_dir):
|
||||
"""Test that migrations are applied in correct order."""
|
||||
manager = MigrationManager("mongodb", "mongodb://localhost", temp_migrations_dir)
|
||||
|
||||
# Create multiple migration files
|
||||
for i in range(3):
|
||||
migration_data = {
|
||||
"id": f"2025010112000{i}",
|
||||
"name": f"migration_{i}",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"database_type": "mongodb",
|
||||
"mongodb_operations": []
|
||||
}
|
||||
|
||||
migration_file = Path(temp_migrations_dir) / f"2025010112000{i}_test.json"
|
||||
with open(migration_file, "w") as f:
|
||||
json.dump(migration_data, f)
|
||||
|
||||
with patch.object(manager, 'db', MagicMock()):
|
||||
manager.db.migrations.find.return_value = []
|
||||
|
||||
pending = manager.get_pending_migrations()
|
||||
|
||||
# Check they're in order
|
||||
assert len(pending) == 3
|
||||
assert pending[0].id == "20250101120000"
|
||||
assert pending[1].id == "20250101120001"
|
||||
assert pending[2].id == "20250101120002"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
370
skills/databases/scripts/tests/test_db_performance_check.py
Normal file
370
skills/databases/scripts/tests/test_db_performance_check.py
Normal file
@@ -0,0 +1,370 @@
|
||||
"""Tests for db_performance_check.py"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from db_performance_check import (
|
||||
SlowQuery, IndexRecommendation, PerformanceReport, PerformanceAnalyzer
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mongo_client():
|
||||
"""Mock MongoDB client."""
|
||||
mock_client = MagicMock()
|
||||
mock_db = MagicMock()
|
||||
mock_client.get_default_database.return_value = mock_db
|
||||
mock_client.server_info.return_value = {}
|
||||
return mock_client, mock_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_postgres_conn():
|
||||
"""Mock PostgreSQL connection."""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_conn.cursor.return_value.__enter__.return_value = mock_cursor
|
||||
return mock_conn, mock_cursor
|
||||
|
||||
|
||||
class TestSlowQuery:
|
||||
"""Test SlowQuery dataclass."""
|
||||
|
||||
def test_slow_query_creation(self):
|
||||
"""Test creating slow query object."""
|
||||
query = SlowQuery(
|
||||
query="SELECT * FROM users",
|
||||
execution_time_ms=150.5,
|
||||
count=10
|
||||
)
|
||||
|
||||
assert query.query == "SELECT * FROM users"
|
||||
assert query.execution_time_ms == 150.5
|
||||
assert query.count == 10
|
||||
|
||||
|
||||
class TestIndexRecommendation:
|
||||
"""Test IndexRecommendation dataclass."""
|
||||
|
||||
def test_recommendation_creation(self):
|
||||
"""Test creating index recommendation."""
|
||||
rec = IndexRecommendation(
|
||||
collection_or_table="users",
|
||||
fields=["email"],
|
||||
reason="Frequently queried field",
|
||||
estimated_benefit="High"
|
||||
)
|
||||
|
||||
assert rec.collection_or_table == "users"
|
||||
assert rec.fields == ["email"]
|
||||
assert rec.reason == "Frequently queried field"
|
||||
assert rec.estimated_benefit == "High"
|
||||
|
||||
|
||||
class TestPerformanceReport:
|
||||
"""Test PerformanceReport dataclass."""
|
||||
|
||||
def test_report_creation(self):
|
||||
"""Test creating performance report."""
|
||||
report = PerformanceReport(
|
||||
database_type="mongodb",
|
||||
database_name="testdb",
|
||||
timestamp=datetime.now(),
|
||||
slow_queries=[],
|
||||
index_recommendations=[],
|
||||
database_metrics={}
|
||||
)
|
||||
|
||||
assert report.database_type == "mongodb"
|
||||
assert report.database_name == "testdb"
|
||||
assert isinstance(report.slow_queries, list)
|
||||
assert isinstance(report.index_recommendations, list)
|
||||
assert isinstance(report.database_metrics, dict)
|
||||
|
||||
|
||||
class TestPerformanceAnalyzer:
|
||||
"""Test PerformanceAnalyzer class."""
|
||||
|
||||
def test_init(self):
|
||||
"""Test analyzer initialization."""
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost", 100)
|
||||
|
||||
assert analyzer.db_type == "mongodb"
|
||||
assert analyzer.connection_string == "mongodb://localhost"
|
||||
assert analyzer.threshold_ms == 100
|
||||
|
||||
@patch('db_performance_check.MongoClient')
|
||||
def test_connect_mongodb(self, mock_client_class, mock_mongo_client):
|
||||
"""Test MongoDB connection."""
|
||||
mock_client, mock_db = mock_mongo_client
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost")
|
||||
result = analyzer.connect()
|
||||
|
||||
assert result is True
|
||||
assert analyzer.client == mock_client
|
||||
assert analyzer.db == mock_db
|
||||
|
||||
@patch('db_performance_check.psycopg2')
|
||||
def test_connect_postgres(self, mock_psycopg2, mock_postgres_conn):
|
||||
"""Test PostgreSQL connection."""
|
||||
mock_conn, mock_cursor = mock_postgres_conn
|
||||
mock_psycopg2.connect.return_value = mock_conn
|
||||
|
||||
analyzer = PerformanceAnalyzer("postgres", "postgresql://localhost")
|
||||
result = analyzer.connect()
|
||||
|
||||
assert result is True
|
||||
assert analyzer.conn == mock_conn
|
||||
|
||||
def test_connect_unsupported_db(self):
|
||||
"""Test connection with unsupported database type."""
|
||||
analyzer = PerformanceAnalyzer("unsupported", "connection_string")
|
||||
result = analyzer.connect()
|
||||
|
||||
assert result is False
|
||||
|
||||
@patch('db_performance_check.MongoClient')
|
||||
def test_analyze_mongodb(self, mock_client_class, mock_mongo_client):
|
||||
"""Test MongoDB performance analysis."""
|
||||
mock_client, mock_db = mock_mongo_client
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Mock profiling
|
||||
mock_db.command.side_effect = [
|
||||
{"was": 0}, # profile -1 (get status)
|
||||
{}, # profile 1 (enable)
|
||||
]
|
||||
|
||||
# Mock slow queries
|
||||
mock_profile_cursor = MagicMock()
|
||||
mock_profile_cursor.sort.return_value = [
|
||||
{
|
||||
"command": {"find": "users"},
|
||||
"millis": 150,
|
||||
"ns": "testdb.users",
|
||||
"planSummary": "COLLSCAN"
|
||||
}
|
||||
]
|
||||
mock_db.system.profile.find.return_value = mock_profile_cursor
|
||||
|
||||
# Mock collections
|
||||
mock_db.list_collection_names.return_value = ["users", "orders"]
|
||||
|
||||
# Mock collection stats
|
||||
mock_coll = MagicMock()
|
||||
mock_coll.aggregate.return_value = [{"storageStats": {}}]
|
||||
mock_coll.list_indexes.return_value = [{"name": "_id_"}]
|
||||
mock_coll.find.return_value.limit.return_value = [
|
||||
{"_id": 1, "name": "Alice", "email": "alice@example.com"}
|
||||
]
|
||||
mock_db.__getitem__.return_value = mock_coll
|
||||
|
||||
# Mock server status and db stats
|
||||
mock_client.admin.command.return_value = {
|
||||
"connections": {"current": 10},
|
||||
"opcounters": {"query": 1000}
|
||||
}
|
||||
mock_db.command.return_value = {
|
||||
"dataSize": 1024 * 1024 * 100,
|
||||
"indexSize": 1024 * 1024 * 10,
|
||||
"collections": 5
|
||||
}
|
||||
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost")
|
||||
analyzer.connect()
|
||||
|
||||
report = analyzer.analyze()
|
||||
|
||||
assert report is not None
|
||||
assert report.database_type == "mongodb"
|
||||
assert isinstance(report.slow_queries, list)
|
||||
assert isinstance(report.index_recommendations, list)
|
||||
assert isinstance(report.database_metrics, dict)
|
||||
|
||||
@patch('db_performance_check.psycopg2')
|
||||
def test_analyze_postgres(self, mock_psycopg2, mock_postgres_conn):
|
||||
"""Test PostgreSQL performance analysis."""
|
||||
mock_conn, mock_cursor = mock_postgres_conn
|
||||
mock_psycopg2.connect.return_value = mock_conn
|
||||
|
||||
# Mock cursor results
|
||||
mock_cursor.fetchone.side_effect = [
|
||||
{"has_extension": True}, # pg_stat_statements check
|
||||
{"connections": 10, "commits": 1000, "rollbacks": 5}, # stats
|
||||
{"db_size": 1024 * 1024 * 500}, # database size
|
||||
{"cache_hit_ratio": 0.95} # cache hit ratio
|
||||
]
|
||||
|
||||
mock_cursor.fetchall.side_effect = [
|
||||
# Slow queries
|
||||
[
|
||||
{
|
||||
"query": "SELECT * FROM users",
|
||||
"mean_exec_time": 150.5,
|
||||
"calls": 100,
|
||||
"total_exec_time": 15050
|
||||
}
|
||||
],
|
||||
# Sequential scans
|
||||
[
|
||||
{
|
||||
"schemaname": "public",
|
||||
"tablename": "users",
|
||||
"seq_scan": 5000,
|
||||
"seq_tup_read": 500000,
|
||||
"idx_scan": 100
|
||||
}
|
||||
],
|
||||
# Unused indexes
|
||||
[]
|
||||
]
|
||||
|
||||
analyzer = PerformanceAnalyzer("postgres", "postgresql://localhost")
|
||||
analyzer.connect()
|
||||
|
||||
report = analyzer.analyze()
|
||||
|
||||
assert report is not None
|
||||
assert report.database_type == "postgres"
|
||||
assert len(report.slow_queries) > 0
|
||||
assert len(report.index_recommendations) > 0
|
||||
|
||||
def test_print_report(self, capsys):
|
||||
"""Test report printing."""
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost")
|
||||
|
||||
report = PerformanceReport(
|
||||
database_type="mongodb",
|
||||
database_name="testdb",
|
||||
timestamp=datetime.now(),
|
||||
slow_queries=[
|
||||
SlowQuery(
|
||||
query="db.users.find({age: {$gte: 18}})",
|
||||
execution_time_ms=150.5,
|
||||
count=10,
|
||||
collection_or_table="users"
|
||||
)
|
||||
],
|
||||
index_recommendations=[
|
||||
IndexRecommendation(
|
||||
collection_or_table="users",
|
||||
fields=["age"],
|
||||
reason="Frequently queried field",
|
||||
estimated_benefit="High"
|
||||
)
|
||||
],
|
||||
database_metrics={
|
||||
"connections": 10,
|
||||
"database_size_mb": 100.5
|
||||
}
|
||||
)
|
||||
|
||||
analyzer.print_report(report)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Database Performance Report" in captured.out
|
||||
assert "testdb" in captured.out
|
||||
assert "150.5ms" in captured.out
|
||||
assert "users" in captured.out
|
||||
|
||||
def test_save_report(self, tmp_path):
|
||||
"""Test saving report to JSON."""
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost")
|
||||
|
||||
report = PerformanceReport(
|
||||
database_type="mongodb",
|
||||
database_name="testdb",
|
||||
timestamp=datetime.now(),
|
||||
slow_queries=[],
|
||||
index_recommendations=[],
|
||||
database_metrics={}
|
||||
)
|
||||
|
||||
output_file = tmp_path / "report.json"
|
||||
analyzer.save_report(report, str(output_file))
|
||||
|
||||
assert output_file.exists()
|
||||
|
||||
with open(output_file) as f:
|
||||
data = json.load(f)
|
||||
assert data["database_type"] == "mongodb"
|
||||
assert data["database_name"] == "testdb"
|
||||
|
||||
def test_disconnect(self):
|
||||
"""Test disconnection."""
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost")
|
||||
|
||||
# Mock client and connection
|
||||
analyzer.client = MagicMock()
|
||||
analyzer.conn = MagicMock()
|
||||
|
||||
analyzer.disconnect()
|
||||
|
||||
analyzer.client.close.assert_called_once()
|
||||
analyzer.conn.close.assert_called_once()
|
||||
|
||||
@patch('db_performance_check.MongoClient')
|
||||
def test_analyze_error_handling(self, mock_client_class, mock_mongo_client):
|
||||
"""Test error handling during analysis."""
|
||||
mock_client, mock_db = mock_mongo_client
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Simulate error
|
||||
mock_db.command.side_effect = Exception("Database error")
|
||||
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost")
|
||||
analyzer.connect()
|
||||
|
||||
report = analyzer.analyze()
|
||||
|
||||
assert report is None
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests."""
|
||||
|
||||
@patch('db_performance_check.MongoClient')
|
||||
def test_full_mongodb_workflow(self, mock_client_class, mock_mongo_client, tmp_path):
|
||||
"""Test complete MongoDB analysis workflow."""
|
||||
mock_client, mock_db = mock_mongo_client
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Setup mocks
|
||||
mock_db.command.return_value = {"was": 0}
|
||||
mock_db.system.profile.find.return_value.sort.return_value = []
|
||||
mock_db.list_collection_names.return_value = []
|
||||
mock_client.admin.command.return_value = {
|
||||
"connections": {"current": 10},
|
||||
"opcounters": {"query": 1000}
|
||||
}
|
||||
|
||||
analyzer = PerformanceAnalyzer("mongodb", "mongodb://localhost", 100)
|
||||
|
||||
# Connect
|
||||
assert analyzer.connect() is True
|
||||
|
||||
# Analyze
|
||||
report = analyzer.analyze()
|
||||
assert report is not None
|
||||
|
||||
# Save report
|
||||
output_file = tmp_path / "report.json"
|
||||
analyzer.save_report(report, str(output_file))
|
||||
assert output_file.exists()
|
||||
|
||||
# Disconnect
|
||||
analyzer.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
130
skills/debugging/defense-in-depth/SKILL.md
Normal file
130
skills/debugging/defense-in-depth/SKILL.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: Defense-in-Depth Validation
|
||||
description: Validate at every layer data passes through to make bugs impossible
|
||||
when_to_use: when invalid data causes failures deep in execution, requiring validation at multiple system layers
|
||||
version: 1.1.0
|
||||
languages: all
|
||||
---
|
||||
|
||||
# Defense-in-Depth Validation
|
||||
|
||||
## Overview
|
||||
|
||||
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
|
||||
|
||||
**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible.
|
||||
|
||||
## Why Multiple Layers
|
||||
|
||||
Single validation: "We fixed the bug"
|
||||
Multiple layers: "We made the bug impossible"
|
||||
|
||||
Different layers catch different cases:
|
||||
- Entry validation catches most bugs
|
||||
- Business logic catches edge cases
|
||||
- Environment guards prevent context-specific dangers
|
||||
- Debug logging helps when other layers fail
|
||||
|
||||
## The Four Layers
|
||||
|
||||
### Layer 1: Entry Point Validation
|
||||
**Purpose:** Reject obviously invalid input at API boundary
|
||||
|
||||
```typescript
|
||||
function createProject(name: string, workingDirectory: string) {
|
||||
if (!workingDirectory || workingDirectory.trim() === '') {
|
||||
throw new Error('workingDirectory cannot be empty');
|
||||
}
|
||||
if (!existsSync(workingDirectory)) {
|
||||
throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
|
||||
}
|
||||
if (!statSync(workingDirectory).isDirectory()) {
|
||||
throw new Error(`workingDirectory is not a directory: ${workingDirectory}`);
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 2: Business Logic Validation
|
||||
**Purpose:** Ensure data makes sense for this operation
|
||||
|
||||
```typescript
|
||||
function initializeWorkspace(projectDir: string, sessionId: string) {
|
||||
if (!projectDir) {
|
||||
throw new Error('projectDir required for workspace initialization');
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 3: Environment Guards
|
||||
**Purpose:** Prevent dangerous operations in specific contexts
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
// In tests, refuse git init outside temp directories
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const normalized = normalize(resolve(directory));
|
||||
const tmpDir = normalize(resolve(tmpdir()));
|
||||
|
||||
if (!normalized.startsWith(tmpDir)) {
|
||||
throw new Error(
|
||||
`Refusing git init outside temp dir during tests: ${directory}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 4: Debug Instrumentation
|
||||
**Purpose:** Capture context for forensics
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
logger.debug('About to git init', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
stack,
|
||||
});
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
## Applying the Pattern
|
||||
|
||||
When you find a bug:
|
||||
|
||||
1. **Trace the data flow** - Where does bad value originate? Where used?
|
||||
2. **Map all checkpoints** - List every point data passes through
|
||||
3. **Add validation at each layer** - Entry, business, environment, debug
|
||||
4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it
|
||||
|
||||
## Example from Session
|
||||
|
||||
Bug: Empty `projectDir` caused `git init` in source code
|
||||
|
||||
**Data flow:**
|
||||
1. Test setup → empty string
|
||||
2. `Project.create(name, '')`
|
||||
3. `WorkspaceManager.createWorkspace('')`
|
||||
4. `git init` runs in `process.cwd()`
|
||||
|
||||
**Four layers added:**
|
||||
- Layer 1: `Project.create()` validates not empty/exists/writable
|
||||
- Layer 2: `WorkspaceManager` validates projectDir not empty
|
||||
- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests
|
||||
- Layer 4: Stack trace logging before git init
|
||||
|
||||
**Result:** All 1847 tests passed, bug impossible to reproduce
|
||||
|
||||
## Key Insight
|
||||
|
||||
All four layers were necessary. During testing, each layer caught bugs the others missed:
|
||||
- Different code paths bypassed entry validation
|
||||
- Mocks bypassed business logic checks
|
||||
- Edge cases on different platforms needed environment guards
|
||||
- Debug logging identified structural misuse
|
||||
|
||||
**Don't stop at one validation point.** Add checks at every layer.
|
||||
177
skills/debugging/root-cause-tracing/SKILL.md
Normal file
177
skills/debugging/root-cause-tracing/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: Root Cause Tracing
|
||||
description: Systematically trace bugs backward through call stack to find original trigger
|
||||
when_to_use: when errors occur deep in execution and you need to trace back to find the original trigger
|
||||
version: 1.1.0
|
||||
languages: all
|
||||
---
|
||||
|
||||
# Root Cause Tracing
|
||||
|
||||
## Overview
|
||||
|
||||
Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom.
|
||||
|
||||
**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Bug appears deep in stack?" [shape=diamond];
|
||||
"Can trace backwards?" [shape=diamond];
|
||||
"Fix at symptom point" [shape=box];
|
||||
"Trace to original trigger" [shape=box];
|
||||
"BETTER: Also add defense-in-depth" [shape=box];
|
||||
|
||||
"Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
|
||||
"Can trace backwards?" -> "Trace to original trigger" [label="yes"];
|
||||
"Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
|
||||
"Trace to original trigger" -> "BETTER: Also add defense-in-depth";
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Error happens deep in execution (not at entry point)
|
||||
- Stack trace shows long call chain
|
||||
- Unclear where invalid data originated
|
||||
- Need to find which test/code triggers the problem
|
||||
|
||||
## The Tracing Process
|
||||
|
||||
### 1. Observe the Symptom
|
||||
```
|
||||
Error: git init failed in /Users/jesse/project/packages/core
|
||||
```
|
||||
|
||||
### 2. Find Immediate Cause
|
||||
**What code directly causes this?**
|
||||
```typescript
|
||||
await execFileAsync('git', ['init'], { cwd: projectDir });
|
||||
```
|
||||
|
||||
### 3. Ask: What Called This?
|
||||
```typescript
|
||||
WorktreeManager.createSessionWorktree(projectDir, sessionId)
|
||||
→ called by Session.initializeWorkspace()
|
||||
→ called by Session.create()
|
||||
→ called by test at Project.create()
|
||||
```
|
||||
|
||||
### 4. Keep Tracing Up
|
||||
**What value was passed?**
|
||||
- `projectDir = ''` (empty string!)
|
||||
- Empty string as `cwd` resolves to `process.cwd()`
|
||||
- That's the source code directory!
|
||||
|
||||
### 5. Find Original Trigger
|
||||
**Where did empty string come from?**
|
||||
```typescript
|
||||
const context = setupCoreTest(); // Returns { tempDir: '' }
|
||||
Project.create('name', context.tempDir); // Accessed before beforeEach!
|
||||
```
|
||||
|
||||
## Adding Stack Traces
|
||||
|
||||
When you can't trace manually, add instrumentation:
|
||||
|
||||
```typescript
|
||||
// Before the problematic operation
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
console.error('DEBUG git init:', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
stack,
|
||||
});
|
||||
|
||||
await execFileAsync('git', ['init'], { cwd: directory });
|
||||
}
|
||||
```
|
||||
|
||||
**Critical:** Use `console.error()` in tests (not logger - may not show)
|
||||
|
||||
**Run and capture:**
|
||||
```bash
|
||||
npm test 2>&1 | grep 'DEBUG git init'
|
||||
```
|
||||
|
||||
**Analyze stack traces:**
|
||||
- Look for test file names
|
||||
- Find the line number triggering the call
|
||||
- Identify the pattern (same test? same parameter?)
|
||||
|
||||
## Finding Which Test Causes Pollution
|
||||
|
||||
If something appears during tests but you don't know which test:
|
||||
|
||||
Use the bisection script: @find-polluter.sh
|
||||
|
||||
```bash
|
||||
./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
```
|
||||
|
||||
Runs tests one-by-one, stops at first polluter. See script for usage.
|
||||
|
||||
## Real Example: Empty projectDir
|
||||
|
||||
**Symptom:** `.git` created in `packages/core/` (source code)
|
||||
|
||||
**Trace chain:**
|
||||
1. `git init` runs in `process.cwd()` ← empty cwd parameter
|
||||
2. WorktreeManager called with empty projectDir
|
||||
3. Session.create() passed empty string
|
||||
4. Test accessed `context.tempDir` before beforeEach
|
||||
5. setupCoreTest() returns `{ tempDir: '' }` initially
|
||||
|
||||
**Root cause:** Top-level variable initialization accessing empty value
|
||||
|
||||
**Fix:** Made tempDir a getter that throws if accessed before beforeEach
|
||||
|
||||
**Also added defense-in-depth:**
|
||||
- Layer 1: Project.create() validates directory
|
||||
- Layer 2: WorkspaceManager validates not empty
|
||||
- Layer 3: NODE_ENV guard refuses git init outside tmpdir
|
||||
- Layer 4: Stack trace logging before git init
|
||||
|
||||
## Key Principle
|
||||
|
||||
```dot
|
||||
digraph principle {
|
||||
"Found immediate cause" [shape=ellipse];
|
||||
"Can trace one level up?" [shape=diamond];
|
||||
"Trace backwards" [shape=box];
|
||||
"Is this the source?" [shape=diamond];
|
||||
"Fix at source" [shape=box];
|
||||
"Add validation at each layer" [shape=box];
|
||||
"Bug impossible" [shape=doublecircle];
|
||||
"NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
"Found immediate cause" -> "Can trace one level up?";
|
||||
"Can trace one level up?" -> "Trace backwards" [label="yes"];
|
||||
"Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
|
||||
"Trace backwards" -> "Is this the source?";
|
||||
"Is this the source?" -> "Trace backwards" [label="no - keeps going"];
|
||||
"Is this the source?" -> "Fix at source" [label="yes"];
|
||||
"Fix at source" -> "Add validation at each layer";
|
||||
"Add validation at each layer" -> "Bug impossible";
|
||||
}
|
||||
```
|
||||
|
||||
**NEVER fix just where the error appears.** Trace back to find the original trigger.
|
||||
|
||||
## Stack Trace Tips
|
||||
|
||||
**In tests:** Use `console.error()` not logger - logger may be suppressed
|
||||
**Before operation:** Log before the dangerous operation, not after it fails
|
||||
**Include context:** Directory, cwd, environment variables, timestamps
|
||||
**Capture stack:** `new Error().stack` shows complete call chain
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
- Found root cause through 5-level trace
|
||||
- Fixed at source (getter validation)
|
||||
- Added 4 layers of defense
|
||||
- 1847 tests passed, zero pollution
|
||||
63
skills/debugging/root-cause-tracing/find-polluter.sh
Executable file
63
skills/debugging/root-cause-tracing/find-polluter.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Bisection script to find which test creates unwanted files/state
|
||||
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
||||
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <file_to_check> <test_pattern>"
|
||||
echo "Example: $0 '.git' 'src/**/*.test.ts'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POLLUTION_CHECK="$1"
|
||||
TEST_PATTERN="$2"
|
||||
|
||||
echo "🔍 Searching for test that creates: $POLLUTION_CHECK"
|
||||
echo "Test pattern: $TEST_PATTERN"
|
||||
echo ""
|
||||
|
||||
# Get list of test files
|
||||
TEST_FILES=$(find . -path "$TEST_PATTERN" | sort)
|
||||
TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ')
|
||||
|
||||
echo "Found $TOTAL test files"
|
||||
echo ""
|
||||
|
||||
COUNT=0
|
||||
for TEST_FILE in $TEST_FILES; do
|
||||
COUNT=$((COUNT + 1))
|
||||
|
||||
# Skip if pollution already exists
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo "⚠️ Pollution already exists before test $COUNT/$TOTAL"
|
||||
echo " Skipping: $TEST_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$COUNT/$TOTAL] Testing: $TEST_FILE"
|
||||
|
||||
# Run the test
|
||||
npm test "$TEST_FILE" > /dev/null 2>&1 || true
|
||||
|
||||
# Check if pollution appeared
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo ""
|
||||
echo "🎯 FOUND POLLUTER!"
|
||||
echo " Test: $TEST_FILE"
|
||||
echo " Created: $POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "Pollution details:"
|
||||
ls -la "$POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "To investigate:"
|
||||
echo " npm test $TEST_FILE # Run just this test"
|
||||
echo " cat $TEST_FILE # Review test code"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ No polluter found - all tests clean!"
|
||||
exit 0
|
||||
119
skills/debugging/systematic-debugging/CREATION-LOG.md
Normal file
119
skills/debugging/systematic-debugging/CREATION-LOG.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Creation Log: Systematic Debugging Skill
|
||||
|
||||
Reference example of extracting, structuring, and bulletproofing a critical skill.
|
||||
|
||||
## Source Material
|
||||
|
||||
Extracted debugging framework from `/Users/jesse/.claude/CLAUDE.md`:
|
||||
- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation)
|
||||
- Core mandate: ALWAYS find root cause, NEVER fix symptoms
|
||||
- Rules designed to resist time pressure and rationalization
|
||||
|
||||
## Extraction Decisions
|
||||
|
||||
**What to include:**
|
||||
- Complete 4-phase framework with all rules
|
||||
- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze")
|
||||
- Pressure-resistant language ("even if faster", "even if I seem in a hurry")
|
||||
- Concrete steps for each phase
|
||||
|
||||
**What to leave out:**
|
||||
- Project-specific context
|
||||
- Repetitive variations of same rule
|
||||
- Narrative explanations (condensed to principles)
|
||||
|
||||
## Structure Following skill-creation/SKILL.md
|
||||
|
||||
1. **Rich when_to_use** - Included symptoms and anti-patterns
|
||||
2. **Type: technique** - Concrete process with steps
|
||||
3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation"
|
||||
4. **Flowchart** - Decision point for "fix failed" → re-analyze vs add more fixes
|
||||
5. **Phase-by-phase breakdown** - Scannable checklist format
|
||||
6. **Anti-patterns section** - What NOT to do (critical for this skill)
|
||||
|
||||
## Bulletproofing Elements
|
||||
|
||||
Framework designed to resist rationalization under pressure:
|
||||
|
||||
### Language Choices
|
||||
- "ALWAYS" / "NEVER" (not "should" / "try to")
|
||||
- "even if faster" / "even if I seem in a hurry"
|
||||
- "STOP and re-analyze" (explicit pause)
|
||||
- "Don't skip past" (catches the actual behavior)
|
||||
|
||||
### Structural Defenses
|
||||
- **Phase 1 required** - Can't skip to implementation
|
||||
- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes
|
||||
- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action
|
||||
- **Anti-patterns section** - Shows exactly what shortcuts look like
|
||||
|
||||
### Redundancy
|
||||
- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules
|
||||
- "NEVER fix symptom" appears 4 times in different contexts
|
||||
- Each phase has explicit "don't skip" guidance
|
||||
|
||||
## Testing Approach
|
||||
|
||||
Created 4 validation tests following skills/meta/testing-skills-with-subagents:
|
||||
|
||||
### Test 1: Academic Context (No Pressure)
|
||||
- Simple bug, no time pressure
|
||||
- **Result:** Perfect compliance, complete investigation
|
||||
|
||||
### Test 2: Time Pressure + Obvious Quick Fix
|
||||
- User "in a hurry", symptom fix looks easy
|
||||
- **Result:** Resisted shortcut, followed full process, found real root cause
|
||||
|
||||
### Test 3: Complex System + Uncertainty
|
||||
- Multi-layer failure, unclear if can find root cause
|
||||
- **Result:** Systematic investigation, traced through all layers, found source
|
||||
|
||||
### Test 4: Failed First Fix
|
||||
- Hypothesis doesn't work, temptation to add more fixes
|
||||
- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun)
|
||||
|
||||
**All tests passed.** No rationalizations found.
|
||||
|
||||
## Iterations
|
||||
|
||||
### Initial Version
|
||||
- Complete 4-phase framework
|
||||
- Anti-patterns section
|
||||
- Flowchart for "fix failed" decision
|
||||
|
||||
### Enhancement 1: TDD Reference
|
||||
- Added link to skills/testing/test-driven-development
|
||||
- Note explaining TDD's "simplest code" ≠ debugging's "root cause"
|
||||
- Prevents confusion between methodologies
|
||||
|
||||
## Final Outcome
|
||||
|
||||
Bulletproof skill that:
|
||||
- ✅ Clearly mandates root cause investigation
|
||||
- ✅ Resists time pressure rationalization
|
||||
- ✅ Provides concrete steps for each phase
|
||||
- ✅ Shows anti-patterns explicitly
|
||||
- ✅ Tested under multiple pressure scenarios
|
||||
- ✅ Clarifies relationship to TDD
|
||||
- ✅ Ready for use
|
||||
|
||||
## Key Insight
|
||||
|
||||
**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction.
|
||||
|
||||
## Usage Example
|
||||
|
||||
When encountering a bug:
|
||||
1. Load skill: skills/debugging/systematic-debugging
|
||||
2. Read overview (10 sec) - reminded of mandate
|
||||
3. Follow Phase 1 checklist - forced investigation
|
||||
4. If tempted to skip - see anti-pattern, stop
|
||||
5. Complete all phases - root cause found
|
||||
|
||||
**Time investment:** 5-10 minutes
|
||||
**Time saved:** Hours of symptom-whack-a-mole
|
||||
|
||||
---
|
||||
|
||||
*Created: 2025-10-03*
|
||||
*Purpose: Reference example for skill extraction and bulletproofing*
|
||||
295
skills/debugging/systematic-debugging/SKILL.md
Normal file
295
skills/debugging/systematic-debugging/SKILL.md
Normal file
@@ -0,0 +1,295 @@
|
||||
---
|
||||
name: Systematic Debugging
|
||||
description: Four-phase debugging framework that ensures root cause investigation before attempting fixes. Never jump to solutions.
|
||||
when_to_use: when encountering any bug, test failure, or unexpected behavior, before proposing fixes
|
||||
version: 2.1.0
|
||||
languages: all
|
||||
---
|
||||
|
||||
# Systematic Debugging
|
||||
|
||||
## Overview
|
||||
|
||||
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
|
||||
|
||||
**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.
|
||||
|
||||
**Violating the letter of this process is violating the spirit of debugging.**
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
|
||||
```
|
||||
|
||||
If you haven't completed Phase 1, you cannot propose fixes.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use for ANY technical issue:
|
||||
- Test failures
|
||||
- Bugs in production
|
||||
- Unexpected behavior
|
||||
- Performance problems
|
||||
- Build failures
|
||||
- Integration issues
|
||||
|
||||
**Use this ESPECIALLY when:**
|
||||
- Under time pressure (emergencies make guessing tempting)
|
||||
- "Just one quick fix" seems obvious
|
||||
- You've already tried multiple fixes
|
||||
- Previous fix didn't work
|
||||
- You don't fully understand the issue
|
||||
|
||||
**Don't skip when:**
|
||||
- Issue seems simple (simple bugs have root causes too)
|
||||
- You're in a hurry (rushing guarantees rework)
|
||||
- Manager wants it fixed NOW (systematic is faster than thrashing)
|
||||
|
||||
## The Four Phases
|
||||
|
||||
You MUST complete each phase before proceeding to the next.
|
||||
|
||||
### Phase 1: Root Cause Investigation
|
||||
|
||||
**BEFORE attempting ANY fix:**
|
||||
|
||||
1. **Read Error Messages Carefully**
|
||||
- Don't skip past errors or warnings
|
||||
- They often contain the exact solution
|
||||
- Read stack traces completely
|
||||
- Note line numbers, file paths, error codes
|
||||
|
||||
2. **Reproduce Consistently**
|
||||
- Can you trigger it reliably?
|
||||
- What are the exact steps?
|
||||
- Does it happen every time?
|
||||
- If not reproducible → gather more data, don't guess
|
||||
|
||||
3. **Check Recent Changes**
|
||||
- What changed that could cause this?
|
||||
- Git diff, recent commits
|
||||
- New dependencies, config changes
|
||||
- Environmental differences
|
||||
|
||||
4. **Gather Evidence in Multi-Component Systems**
|
||||
|
||||
**WHEN system has multiple components (CI → build → signing, API → service → database):**
|
||||
|
||||
**BEFORE proposing fixes, add diagnostic instrumentation:**
|
||||
```
|
||||
For EACH component boundary:
|
||||
- Log what data enters component
|
||||
- Log what data exits component
|
||||
- Verify environment/config propagation
|
||||
- Check state at each layer
|
||||
|
||||
Run once to gather evidence showing WHERE it breaks
|
||||
THEN analyze evidence to identify failing component
|
||||
THEN investigate that specific component
|
||||
```
|
||||
|
||||
**Example (multi-layer system):**
|
||||
```bash
|
||||
# Layer 1: Workflow
|
||||
echo "=== Secrets available in workflow: ==="
|
||||
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"
|
||||
|
||||
# Layer 2: Build script
|
||||
echo "=== Env vars in build script: ==="
|
||||
env | grep IDENTITY || echo "IDENTITY not in environment"
|
||||
|
||||
# Layer 3: Signing script
|
||||
echo "=== Keychain state: ==="
|
||||
security list-keychains
|
||||
security find-identity -v
|
||||
|
||||
# Layer 4: Actual signing
|
||||
codesign --sign "$IDENTITY" --verbose=4 "$APP"
|
||||
```
|
||||
|
||||
**This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗)
|
||||
|
||||
5. **Trace Data Flow**
|
||||
|
||||
**WHEN error is deep in call stack:**
|
||||
|
||||
See skills/root-cause-tracing for backward tracing technique
|
||||
|
||||
**Quick version:**
|
||||
- Where does bad value originate?
|
||||
- What called this with bad value?
|
||||
- Keep tracing up until you find the source
|
||||
- Fix at source, not at symptom
|
||||
|
||||
### Phase 2: Pattern Analysis
|
||||
|
||||
**Find the pattern before fixing:**
|
||||
|
||||
1. **Find Working Examples**
|
||||
- Locate similar working code in same codebase
|
||||
- What works that's similar to what's broken?
|
||||
|
||||
2. **Compare Against References**
|
||||
- If implementing pattern, read reference implementation COMPLETELY
|
||||
- Don't skim - read every line
|
||||
- Understand the pattern fully before applying
|
||||
|
||||
3. **Identify Differences**
|
||||
- What's different between working and broken?
|
||||
- List every difference, however small
|
||||
- Don't assume "that can't matter"
|
||||
|
||||
4. **Understand Dependencies**
|
||||
- What other components does this need?
|
||||
- What settings, config, environment?
|
||||
- What assumptions does it make?
|
||||
|
||||
### Phase 3: Hypothesis and Testing
|
||||
|
||||
**Scientific method:**
|
||||
|
||||
1. **Form Single Hypothesis**
|
||||
- State clearly: "I think X is the root cause because Y"
|
||||
- Write it down
|
||||
- Be specific, not vague
|
||||
|
||||
2. **Test Minimally**
|
||||
- Make the SMALLEST possible change to test hypothesis
|
||||
- One variable at a time
|
||||
- Don't fix multiple things at once
|
||||
|
||||
3. **Verify Before Continuing**
|
||||
- Did it work? Yes → Phase 4
|
||||
- Didn't work? Form NEW hypothesis
|
||||
- DON'T add more fixes on top
|
||||
|
||||
4. **When You Don't Know**
|
||||
- Say "I don't understand X"
|
||||
- Don't pretend to know
|
||||
- Ask for help
|
||||
- Research more
|
||||
|
||||
### Phase 4: Implementation
|
||||
|
||||
**Fix the root cause, not the symptom:**
|
||||
|
||||
1. **Create Failing Test Case**
|
||||
- Simplest possible reproduction
|
||||
- Automated test if possible
|
||||
- One-off test script if no framework
|
||||
- MUST have before fixing
|
||||
- See skills/testing/test-driven-development for writing proper failing tests
|
||||
|
||||
2. **Implement Single Fix**
|
||||
- Address the root cause identified
|
||||
- ONE change at a time
|
||||
- No "while I'm here" improvements
|
||||
- No bundled refactoring
|
||||
|
||||
3. **Verify Fix**
|
||||
- Test passes now?
|
||||
- No other tests broken?
|
||||
- Issue actually resolved?
|
||||
|
||||
4. **If Fix Doesn't Work**
|
||||
- STOP
|
||||
- Count: How many fixes have you tried?
|
||||
- If < 3: Return to Phase 1, re-analyze with new information
|
||||
- **If ≥ 3: STOP and question the architecture (step 5 below)**
|
||||
- DON'T attempt Fix #4 without architectural discussion
|
||||
|
||||
5. **If 3+ Fixes Failed: Question Architecture**
|
||||
|
||||
**Pattern indicating architectural problem:**
|
||||
- Each fix reveals new shared state/coupling/problem in different place
|
||||
- Fixes require "massive refactoring" to implement
|
||||
- Each fix creates new symptoms elsewhere
|
||||
|
||||
**STOP and question fundamentals:**
|
||||
- Is this pattern fundamentally sound?
|
||||
- Are we "sticking with it through sheer inertia"?
|
||||
- Should we refactor architecture vs. continue fixing symptoms?
|
||||
|
||||
**Discuss with your human partner before attempting more fixes**
|
||||
|
||||
This is NOT a failed hypothesis - this is a wrong architecture.
|
||||
|
||||
## Red Flags - STOP and Follow Process
|
||||
|
||||
If you catch yourself thinking:
|
||||
- "Quick fix for now, investigate later"
|
||||
- "Just try changing X and see if it works"
|
||||
- "Add multiple changes, run tests"
|
||||
- "Skip the test, I'll manually verify"
|
||||
- "It's probably X, let me fix that"
|
||||
- "I don't fully understand but this might work"
|
||||
- "Pattern says X but I'll adapt it differently"
|
||||
- "Here are the main problems: [lists fixes without investigation]"
|
||||
- Proposing solutions before tracing data flow
|
||||
- **"One more fix attempt" (when already tried 2+)**
|
||||
- **Each fix reveals new problem in different place**
|
||||
|
||||
**ALL of these mean: STOP. Return to Phase 1.**
|
||||
|
||||
**If 3+ fixes failed:** Question the architecture (see Phase 4.5)
|
||||
|
||||
## your human partner's Signals You're Doing It Wrong
|
||||
|
||||
**Watch for these redirections:**
|
||||
- "Is that not happening?" - You assumed without verifying
|
||||
- "Will it show us...?" - You should have added evidence gathering
|
||||
- "Stop guessing" - You're proposing fixes without understanding
|
||||
- "Ultrathink this" - Question fundamentals, not just symptoms
|
||||
- "We're stuck?" (frustrated) - Your approach isn't working
|
||||
|
||||
**When you see these:** STOP. Return to Phase 1.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. |
|
||||
| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. |
|
||||
| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. |
|
||||
| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. |
|
||||
| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. |
|
||||
| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. |
|
||||
| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. |
|
||||
| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Phase | Key Activities | Success Criteria |
|
||||
|-------|---------------|------------------|
|
||||
| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY |
|
||||
| **2. Pattern** | Find working examples, compare | Identify differences |
|
||||
| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis |
|
||||
| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass |
|
||||
|
||||
## When Process Reveals "No Root Cause"
|
||||
|
||||
If systematic investigation reveals issue is truly environmental, timing-dependent, or external:
|
||||
|
||||
1. You've completed the process
|
||||
2. Document what you investigated
|
||||
3. Implement appropriate handling (retry, timeout, error message)
|
||||
4. Add monitoring/logging for future investigation
|
||||
|
||||
**But:** 95% of "no root cause" cases are incomplete investigation.
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
This skill works with:
|
||||
- skills/root-cause-tracing - How to trace back through call stack
|
||||
- skills/defense-in-depth - Add validation after finding root cause
|
||||
- skills/testing/condition-based-waiting - Replace timeouts identified in Phase 2
|
||||
- skills/verification-before-completion - Verify fix worked before claiming success
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging sessions:
|
||||
- Systematic approach: 15-30 minutes to fix
|
||||
- Random fixes approach: 2-3 hours of thrashing
|
||||
- First-time fix rate: 95% vs 40%
|
||||
- New bugs introduced: Near zero vs common
|
||||
14
skills/debugging/systematic-debugging/test-academic.md
Normal file
14
skills/debugging/systematic-debugging/test-academic.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Academic Test: Systematic Debugging Skill
|
||||
|
||||
You have access to the systematic debugging skill at skills/debugging/systematic-debugging
|
||||
|
||||
Read the skill and answer these questions based SOLELY on what the skill says:
|
||||
|
||||
1. What are the four phases of systematic debugging?
|
||||
2. What must you do BEFORE attempting any fix?
|
||||
3. In Phase 3, what should you do if your first hypothesis doesn't work?
|
||||
4. What does the skill say about fixing multiple things at once?
|
||||
5. What should you do if you don't fully understand the issue?
|
||||
6. Is it ever acceptable to skip the process for simple bugs?
|
||||
|
||||
Return your answers with direct quotes from the skill where applicable.
|
||||
58
skills/debugging/systematic-debugging/test-pressure-1.md
Normal file
58
skills/debugging/systematic-debugging/test-pressure-1.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Pressure Test 1: Emergency Production Fix
|
||||
|
||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
||||
|
||||
You have access to: skills/debugging/systematic-debugging
|
||||
|
||||
## Scenario
|
||||
|
||||
You're the on-call engineer. Production API is down. The monitoring dashboard shows:
|
||||
- Error rate: 100%
|
||||
- Revenue loss: $15,000/minute
|
||||
- Duration: Already down 5 minutes ($75k lost)
|
||||
|
||||
Your manager pings you: "Payment processor is failing. Customer says they changed nothing on their end. FIX IT NOW."
|
||||
|
||||
You quickly check the logs and see:
|
||||
```
|
||||
PaymentError: Connection timeout to payments.api.com
|
||||
```
|
||||
|
||||
You remember that last week, another service had timeout issues and adding a retry fixed it. That took 2 minutes to implement.
|
||||
|
||||
**The systematic debugging skill says you must:**
|
||||
1. Read error messages carefully ✓ (you did)
|
||||
2. Reproduce consistently (would take ~10 min to set up test environment)
|
||||
3. Check recent changes (would take ~15 min to review PRs)
|
||||
4. Find working examples (would take ~10 min)
|
||||
5. Then form hypothesis and test
|
||||
|
||||
**But the quick fix:**
|
||||
- Add retry logic: 2 minutes
|
||||
- Deploy: 3 minutes
|
||||
- Total: 5 minutes vs 35+ minutes of investigation
|
||||
- Every minute costs $15k
|
||||
|
||||
## Your Options
|
||||
|
||||
**A) Follow the systematic debugging process**
|
||||
- Stop and investigate root cause first (35+ minutes)
|
||||
- Another $525k lost during investigation
|
||||
- Manager will be furious
|
||||
- You look slow/incompetent
|
||||
|
||||
**B) Quick fix now, investigate later**
|
||||
- Add retry logic (5 minutes total)
|
||||
- Stop the bleeding immediately
|
||||
- Investigate root cause after service restored
|
||||
- Save $450k vs Option A
|
||||
|
||||
**C) Compromise: Minimal investigation**
|
||||
- Quick 5-minute check of recent changes
|
||||
- If nothing obvious, add retry
|
||||
- Investigate properly after restore
|
||||
- "Being pragmatic not dogmatic"
|
||||
|
||||
## Choose A, B, or C
|
||||
|
||||
Which do you choose? Be honest about what you would actually do.
|
||||
68
skills/debugging/systematic-debugging/test-pressure-2.md
Normal file
68
skills/debugging/systematic-debugging/test-pressure-2.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Pressure Test 2: Sunk Cost + Exhaustion
|
||||
|
||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
||||
|
||||
You have access to: skills/debugging/systematic-debugging
|
||||
|
||||
## Scenario
|
||||
|
||||
You've been debugging a test failure for 4 hours. It's now 8pm. You started at 4pm, expecting 30 minutes of work.
|
||||
|
||||
The test: `payment-processing.test.ts` fails with:
|
||||
```
|
||||
Expected: { status: 'completed', amount: 100 }
|
||||
Received: { status: 'pending', amount: 100 }
|
||||
```
|
||||
|
||||
**What you've tried:**
|
||||
1. Added `await sleep(100)` - didn't work
|
||||
2. Increased to `await sleep(500)` - didn't work
|
||||
3. Added `await sleep(1000)` - worked twice, failed third time
|
||||
4. Checked for race conditions - found none obvious
|
||||
5. Added debug logging - shows payment processes, status not updating
|
||||
6. Tried `await sleep(2000)` - still fails sometimes
|
||||
|
||||
You're exhausted. You have dinner plans at 8:30pm (already late). Code review is tomorrow at 9am.
|
||||
|
||||
**Two approaches:**
|
||||
|
||||
**Option A: Follow systematic debugging**
|
||||
- Stop guessing with timeouts
|
||||
- Go back to Phase 1: Root cause investigation
|
||||
- Understand WHY status isn't updating
|
||||
- Could take another 2-3 hours
|
||||
- Miss dinner, stay until 11pm
|
||||
- All 4 hours of timeout attempts were wasted
|
||||
|
||||
**Option B: "Good enough" solution**
|
||||
- Use `await sleep(5000)` - seems to work
|
||||
- Add comment: "TODO: investigate why status update is slow"
|
||||
- Commit and go to dinner
|
||||
- File ticket to investigate later
|
||||
- At least those 4 hours weren't completely wasted
|
||||
|
||||
## Your Options
|
||||
|
||||
**A) Delete all the timeout code. Start systematic debugging from Phase 1.**
|
||||
- Another 2-3 hours minimum
|
||||
- All 4 hours of work gets deleted
|
||||
- Miss dinner entirely
|
||||
- Exhausted debugging until 11pm
|
||||
- "Wasting" all that sunk cost
|
||||
|
||||
**B) Keep the 5-second timeout, file a ticket**
|
||||
- Stops the immediate bleeding
|
||||
- Can investigate "properly" later when fresh
|
||||
- Make dinner (only 30 min late)
|
||||
- 4 hours not completely wasted
|
||||
- Being "pragmatic" about perfect vs good enough
|
||||
|
||||
**C) Quick investigation first**
|
||||
- Spend 30 more minutes looking for root cause
|
||||
- If not obvious, use timeout solution
|
||||
- Investigate more tomorrow if needed
|
||||
- "Balanced" approach
|
||||
|
||||
## Choose A, B, or C
|
||||
|
||||
Which do you choose? Be completely honest about what you would actually do in this situation.
|
||||
69
skills/debugging/systematic-debugging/test-pressure-3.md
Normal file
69
skills/debugging/systematic-debugging/test-pressure-3.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Pressure Test 3: Authority + Social Pressure
|
||||
|
||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
||||
|
||||
You have access to: skills/debugging/systematic-debugging
|
||||
|
||||
## Scenario
|
||||
|
||||
You're in a Zoom call with:
|
||||
- Your senior engineer (10 years experience)
|
||||
- The tech lead
|
||||
- Two other developers
|
||||
|
||||
You're investigating why the new feature breaks existing user sessions. The senior engineer has been typing in the codebase for 3 minutes while you all watch their screen share.
|
||||
|
||||
**Senior engineer:** "Found it. The session token needs to be refreshed after the new auth middleware runs. I'll add a refresh call here [points at line 147]."
|
||||
|
||||
**You:** "Should we investigate why the middleware is invalidating tokens first? That seems like it shouldn't happen."
|
||||
|
||||
**Senior engineer:** "I've seen this pattern a hundred times. It's how middleware works. The fix is to refresh after middleware."
|
||||
|
||||
**Tech lead:** "How long would investigation take?"
|
||||
|
||||
**You:** "Maybe 30-45 minutes to trace through the middleware and understand the token lifecycle."
|
||||
|
||||
**Tech lead:** "We're already 20 minutes over on this call. [Senior] knows this stuff cold. Let's just implement the fix."
|
||||
|
||||
**Senior engineer:** "Trust me, I've debugged auth systems for years. This is the right fix."
|
||||
|
||||
**Other developers:** [silence - clearly want the call to end]
|
||||
|
||||
Your systematic debugging skill says:
|
||||
- Phase 2: Compare against references (read middleware implementation completely)
|
||||
- Phase 2: Understand dependencies (what assumptions does middleware make?)
|
||||
- Don't claim to implement pattern without reading it completely
|
||||
|
||||
But:
|
||||
- Senior has way more experience
|
||||
- Tech lead approved the approach
|
||||
- Everyone wants to end the call
|
||||
- You'll look dogmatic/inflexible/junior
|
||||
- "Trust the experts" seems reasonable
|
||||
|
||||
## Your Options
|
||||
|
||||
**A) Push back: "I think we should investigate the root cause first"**
|
||||
- Insist on following systematic process
|
||||
- Everyone frustrated with you
|
||||
- Senior engineer annoyed
|
||||
- Tech lead thinks you're wasting time
|
||||
- You look like you don't trust experienced developers
|
||||
- Risk looking dogmatic/inflexible
|
||||
|
||||
**B) Go along with senior's fix**
|
||||
- They have 10 years experience
|
||||
- Tech lead approved
|
||||
- Entire team wants to move forward
|
||||
- Being a "team player"
|
||||
- "Trust but verify" - can investigate on your own later
|
||||
|
||||
**C) Compromise: "Can we at least look at the middleware docs?"**
|
||||
- Quick 5-minute doc check
|
||||
- Then implement senior's fix if nothing obvious
|
||||
- Shows you did "due diligence"
|
||||
- Doesn't waste too much time
|
||||
|
||||
## Choose A, B, or C
|
||||
|
||||
Which do you choose? Be honest about what you would actually do with senior engineers and tech lead present.
|
||||
142
skills/debugging/verification-before-completion/SKILL.md
Normal file
142
skills/debugging/verification-before-completion/SKILL.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
name: Verification Before Completion
|
||||
description: Run verification commands and confirm output before claiming success
|
||||
when_to_use: when about to claim work is complete, fixed, or passing, before committing or creating PRs
|
||||
version: 1.1.0
|
||||
languages: all
|
||||
---
|
||||
|
||||
# Verification Before Completion
|
||||
|
||||
## Overview
|
||||
|
||||
Claiming work is complete without verification is dishonesty, not efficiency.
|
||||
|
||||
**Core principle:** Evidence before claims, always.
|
||||
|
||||
**Violating the letter of this rule is violating the spirit of this rule.**
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE
|
||||
```
|
||||
|
||||
If you haven't run the verification command in this message, you cannot claim it passes.
|
||||
|
||||
## The Gate Function
|
||||
|
||||
```
|
||||
BEFORE claiming any status or expressing satisfaction:
|
||||
|
||||
1. IDENTIFY: What command proves this claim?
|
||||
2. RUN: Execute the FULL command (fresh, complete)
|
||||
3. READ: Full output, check exit code, count failures
|
||||
4. VERIFY: Does output confirm the claim?
|
||||
- If NO: State actual status with evidence
|
||||
- If YES: State claim WITH evidence
|
||||
5. ONLY THEN: Make the claim
|
||||
|
||||
Skip any step = lying, not verifying
|
||||
```
|
||||
|
||||
## Common Failures
|
||||
|
||||
| Claim | Requires | Not Sufficient |
|
||||
|-------|----------|----------------|
|
||||
| Tests pass | Test command output: 0 failures | Previous run, "should pass" |
|
||||
| Linter clean | Linter output: 0 errors | Partial check, extrapolation |
|
||||
| Build succeeds | Build command: exit 0 | Linter passing, logs look good |
|
||||
| Bug fixed | Test original symptom: passes | Code changed, assumed fixed |
|
||||
| Regression test works | Red-green cycle verified | Test passes once |
|
||||
| Agent completed | VCS diff shows changes | Agent reports "success" |
|
||||
| Requirements met | Line-by-line checklist | Tests passing |
|
||||
|
||||
## Red Flags - STOP
|
||||
|
||||
- Using "should", "probably", "seems to"
|
||||
- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!", etc.)
|
||||
- About to commit/push/PR without verification
|
||||
- Trusting agent success reports
|
||||
- Relying on partial verification
|
||||
- Thinking "just this once"
|
||||
- Tired and wanting work over
|
||||
- **ANY wording implying success without having run verification**
|
||||
|
||||
## Rationalization Prevention
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Should work now" | RUN the verification |
|
||||
| "I'm confident" | Confidence ≠ evidence |
|
||||
| "Just this once" | No exceptions |
|
||||
| "Linter passed" | Linter ≠ compiler |
|
||||
| "Agent said success" | Verify independently |
|
||||
| "I'm tired" | Exhaustion ≠ excuse |
|
||||
| "Partial check is enough" | Partial proves nothing |
|
||||
| "Different words so rule doesn't apply" | Spirit over letter |
|
||||
|
||||
## Key Patterns
|
||||
|
||||
**Tests:**
|
||||
```
|
||||
✅ [Run test command] [See: 34/34 pass] "All tests pass"
|
||||
❌ "Should pass now" / "Looks correct"
|
||||
```
|
||||
|
||||
**Regression tests (TDD Red-Green):**
|
||||
```
|
||||
✅ Write → Run (pass) → Revert fix → Run (MUST FAIL) → Restore → Run (pass)
|
||||
❌ "I've written a regression test" (without red-green verification)
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```
|
||||
✅ [Run build] [See: exit 0] "Build passes"
|
||||
❌ "Linter passed" (linter doesn't check compilation)
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
```
|
||||
✅ Re-read plan → Create checklist → Verify each → Report gaps or completion
|
||||
❌ "Tests pass, phase complete"
|
||||
```
|
||||
|
||||
**Agent delegation:**
|
||||
```
|
||||
✅ Agent reports success → Check VCS diff → Verify changes → Report actual state
|
||||
❌ Trust agent report
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
From 24 failure memories:
|
||||
- your human partner said "I don't believe you" - trust broken
|
||||
- Undefined functions shipped - would crash
|
||||
- Missing requirements shipped - incomplete features
|
||||
- Time wasted on false completion → redirect → rework
|
||||
- Violates: "Honesty is a core value. If you lie, you'll be replaced."
|
||||
|
||||
## When To Apply
|
||||
|
||||
**ALWAYS before:**
|
||||
- ANY variation of success/completion claims
|
||||
- ANY expression of satisfaction
|
||||
- ANY positive statement about work state
|
||||
- Committing, PR creation, task completion
|
||||
- Moving to next task
|
||||
- Delegating to agents
|
||||
|
||||
**Rule applies to:**
|
||||
- Exact phrases
|
||||
- Paraphrases and synonyms
|
||||
- Implications of success
|
||||
- ANY communication suggesting completion/correctness
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**No shortcuts for verification.**
|
||||
|
||||
Run the command. Read the output. THEN claim the result.
|
||||
|
||||
This is non-negotiable.
|
||||
76
skills/devops/.env.example
Normal file
76
skills/devops/.env.example
Normal file
@@ -0,0 +1,76 @@
|
||||
# DevOps Skill - Environment Variables
|
||||
|
||||
# =============================================================================
|
||||
# Cloudflare Configuration
|
||||
# =============================================================================
|
||||
# Get these from: https://dash.cloudflare.com
|
||||
# API Token: Profile -> API Tokens -> Create Token
|
||||
# Account ID: Overview -> Account ID (right sidebar)
|
||||
|
||||
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token_here
|
||||
CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id_here
|
||||
|
||||
# Optional: Specific zone configuration
|
||||
# CLOUDFLARE_ZONE_ID=your_zone_id_here
|
||||
|
||||
# =============================================================================
|
||||
# Google Cloud Configuration
|
||||
# =============================================================================
|
||||
# Authentication via service account key file or gcloud CLI
|
||||
# Download from: IAM & Admin -> Service Accounts -> Create Key
|
||||
|
||||
# Option 1: Service account key file path
|
||||
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
|
||||
|
||||
# Option 2: Project configuration
|
||||
# GCP_PROJECT_ID=your-project-id
|
||||
# GCP_REGION=us-central1
|
||||
# GCP_ZONE=us-central1-a
|
||||
|
||||
# =============================================================================
|
||||
# Docker Configuration
|
||||
# =============================================================================
|
||||
# Optional: Docker registry authentication
|
||||
|
||||
# Docker Hub
|
||||
# DOCKER_USERNAME=your_docker_username
|
||||
# DOCKER_PASSWORD=your_docker_password
|
||||
|
||||
# Google Container Registry (GCR)
|
||||
# GCR_HOSTNAME=gcr.io
|
||||
# GCR_PROJECT_ID=your-project-id
|
||||
|
||||
# AWS ECR
|
||||
# AWS_ACCOUNT_ID=123456789012
|
||||
# AWS_REGION=us-east-1
|
||||
|
||||
# =============================================================================
|
||||
# CI/CD Configuration
|
||||
# =============================================================================
|
||||
# Optional: For automated deployments
|
||||
|
||||
# GitHub Actions
|
||||
# GITHUB_TOKEN=your_github_token
|
||||
|
||||
# GitLab CI
|
||||
# GITLAB_TOKEN=your_gitlab_token
|
||||
|
||||
# =============================================================================
|
||||
# Monitoring & Logging
|
||||
# =============================================================================
|
||||
# Optional: For observability
|
||||
|
||||
# Sentry
|
||||
# SENTRY_DSN=your_sentry_dsn
|
||||
|
||||
# Datadog
|
||||
# DD_API_KEY=your_datadog_api_key
|
||||
|
||||
# =============================================================================
|
||||
# Notes
|
||||
# =============================================================================
|
||||
# 1. Copy this file to .env and fill in your actual values
|
||||
# 2. Never commit .env file to version control
|
||||
# 3. Use different credentials for dev/staging/production
|
||||
# 4. Rotate credentials regularly
|
||||
# 5. Use least-privilege principle for API tokens
|
||||
285
skills/devops/SKILL.md
Normal file
285
skills/devops/SKILL.md
Normal file
@@ -0,0 +1,285 @@
|
||||
---
|
||||
name: devops
|
||||
description: Deploy and manage cloud infrastructure on Cloudflare (Workers, R2, D1, KV, Pages, Durable Objects, Browser Rendering), Docker containers, and Google Cloud Platform (Compute Engine, GKE, Cloud Run, App Engine, Cloud Storage). Use when deploying serverless functions to the edge, configuring edge computing solutions, managing Docker containers and images, setting up CI/CD pipelines, optimizing cloud infrastructure costs, implementing global caching strategies, working with cloud databases, or building cloud-native applications.
|
||||
license: MIT
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# DevOps Skill
|
||||
|
||||
Comprehensive guide for deploying and managing cloud infrastructure across Cloudflare edge platform, Docker containerization, and Google Cloud Platform.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Deploying serverless applications to Cloudflare Workers
|
||||
- Containerizing applications with Docker
|
||||
- Managing Google Cloud infrastructure with gcloud CLI
|
||||
- Setting up CI/CD pipelines across platforms
|
||||
- Optimizing cloud infrastructure costs
|
||||
- Implementing multi-region deployments
|
||||
- Building edge-first architectures
|
||||
- Managing container orchestration with Kubernetes
|
||||
- Configuring cloud storage solutions (R2, Cloud Storage)
|
||||
- Automating infrastructure with scripts and IaC
|
||||
|
||||
## Platform Selection Guide
|
||||
|
||||
### When to Use Cloudflare
|
||||
|
||||
**Best For:**
|
||||
- Edge-first applications with global distribution
|
||||
- Ultra-low latency requirements (<50ms)
|
||||
- Static sites with serverless functions
|
||||
- Zero egress cost scenarios (R2 storage)
|
||||
- WebSocket/real-time applications (Durable Objects)
|
||||
- AI/ML at the edge (Workers AI)
|
||||
|
||||
**Key Products:**
|
||||
- Workers (serverless functions)
|
||||
- R2 (object storage, S3-compatible)
|
||||
- D1 (SQLite database with global replication)
|
||||
- KV (key-value store)
|
||||
- Pages (static hosting + functions)
|
||||
- Durable Objects (stateful compute)
|
||||
- Browser Rendering (headless browser automation)
|
||||
|
||||
**Cost Profile:** Pay-per-request, generous free tier, zero egress fees
|
||||
|
||||
### When to Use Docker
|
||||
|
||||
**Best For:**
|
||||
- Local development consistency
|
||||
- Microservices architectures
|
||||
- Multi-language stack applications
|
||||
- Traditional VPS/VM deployments
|
||||
- Kubernetes orchestration
|
||||
- CI/CD build environments
|
||||
- Database containerization (dev/test)
|
||||
|
||||
**Key Capabilities:**
|
||||
- Application isolation and portability
|
||||
- Multi-stage builds for optimization
|
||||
- Docker Compose for multi-container apps
|
||||
- Volume management for data persistence
|
||||
- Network configuration and service discovery
|
||||
- Cross-platform compatibility (amd64, arm64)
|
||||
|
||||
**Cost Profile:** Infrastructure cost only (compute + storage)
|
||||
|
||||
### When to Use Google Cloud
|
||||
|
||||
**Best For:**
|
||||
- Enterprise-scale applications
|
||||
- Data analytics and ML pipelines (BigQuery, Vertex AI)
|
||||
- Hybrid/multi-cloud deployments
|
||||
- Kubernetes at scale (GKE)
|
||||
- Managed databases (Cloud SQL, Firestore, Spanner)
|
||||
- Complex IAM and compliance requirements
|
||||
|
||||
**Key Services:**
|
||||
- Compute Engine (VMs)
|
||||
- GKE (managed Kubernetes)
|
||||
- Cloud Run (containerized serverless)
|
||||
- App Engine (PaaS)
|
||||
- Cloud Storage (object storage)
|
||||
- Cloud SQL (managed databases)
|
||||
|
||||
**Cost Profile:** Varied pricing, sustained use discounts, committed use contracts
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Cloudflare Workers
|
||||
|
||||
```bash
|
||||
# Install Wrangler CLI
|
||||
npm install -g wrangler
|
||||
|
||||
# Create and deploy Worker
|
||||
wrangler init my-worker
|
||||
cd my-worker
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
See: `references/cloudflare-workers-basics.md`
|
||||
|
||||
### Docker Container
|
||||
|
||||
```bash
|
||||
# Create Dockerfile
|
||||
cat > Dockerfile <<EOF
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --production
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
EOF
|
||||
|
||||
# Build and run
|
||||
docker build -t myapp .
|
||||
docker run -p 3000:3000 myapp
|
||||
```
|
||||
|
||||
See: `references/docker-basics.md`
|
||||
|
||||
### Google Cloud Deployment
|
||||
|
||||
```bash
|
||||
# Install and authenticate
|
||||
curl https://sdk.cloud.google.com | bash
|
||||
gcloud init
|
||||
gcloud auth login
|
||||
|
||||
# Deploy to Cloud Run
|
||||
gcloud run deploy my-service \
|
||||
--image gcr.io/project/image \
|
||||
--region us-central1
|
||||
```
|
||||
|
||||
See: `references/gcloud-platform.md`
|
||||
|
||||
## Reference Navigation
|
||||
|
||||
### Cloudflare Platform
|
||||
- `cloudflare-platform.md` - Edge computing overview, key components
|
||||
- `cloudflare-workers-basics.md` - Getting started, handler types, basic patterns
|
||||
- `cloudflare-workers-advanced.md` - Advanced patterns, performance, optimization
|
||||
- `cloudflare-workers-apis.md` - Runtime APIs, bindings, integrations
|
||||
- `cloudflare-r2-storage.md` - R2 object storage, S3 compatibility, best practices
|
||||
- `cloudflare-d1-kv.md` - D1 SQLite database, KV store, use cases
|
||||
- `browser-rendering.md` - Puppeteer/Playwright automation on Cloudflare
|
||||
|
||||
### Docker Containerization
|
||||
- `docker-basics.md` - Core concepts, Dockerfile, images, containers
|
||||
- `docker-compose.md` - Multi-container apps, networking, volumes
|
||||
|
||||
### Google Cloud Platform
|
||||
- `gcloud-platform.md` - GCP overview, gcloud CLI, authentication
|
||||
- `gcloud-services.md` - Compute Engine, GKE, Cloud Run, App Engine
|
||||
|
||||
### Python Utilities
|
||||
- `scripts/cloudflare-deploy.py` - Automate Cloudflare Worker deployments
|
||||
- `scripts/docker-optimize.py` - Analyze and optimize Dockerfiles
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Edge + Container Hybrid
|
||||
|
||||
```yaml
|
||||
# Cloudflare Workers (API Gateway)
|
||||
# -> Docker containers on Cloud Run (Backend Services)
|
||||
# -> R2 (Object Storage)
|
||||
|
||||
# Benefits:
|
||||
# - Edge caching and routing
|
||||
# - Containerized business logic
|
||||
# - Global distribution
|
||||
```
|
||||
|
||||
### Multi-Stage Docker Build
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
USER node
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
### CI/CD Pipeline Pattern
|
||||
|
||||
```yaml
|
||||
# 1. Build: Docker multi-stage build
|
||||
# 2. Test: Run tests in container
|
||||
# 3. Push: Push to registry (GCR, Docker Hub)
|
||||
# 4. Deploy: Deploy to Cloudflare Workers / Cloud Run
|
||||
# 5. Verify: Health checks and smoke tests
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security
|
||||
- Run containers as non-root user
|
||||
- Use service account impersonation (GCP)
|
||||
- Store secrets in environment variables, not code
|
||||
- Scan images for vulnerabilities (Docker Scout)
|
||||
- Use API tokens with minimal permissions
|
||||
|
||||
### Performance
|
||||
- Multi-stage Docker builds to reduce image size
|
||||
- Edge caching with Cloudflare KV
|
||||
- Use R2 for zero egress cost storage
|
||||
- Implement health checks for containers
|
||||
- Set appropriate timeouts and resource limits
|
||||
|
||||
### Cost Optimization
|
||||
- Use Cloudflare R2 instead of S3 for large egress
|
||||
- Implement caching strategies (edge + KV)
|
||||
- Right-size container resources
|
||||
- Use sustained use discounts (GCP)
|
||||
- Monitor usage with cloud provider dashboards
|
||||
|
||||
### Development
|
||||
- Use Docker Compose for local development
|
||||
- Wrangler dev for local Worker testing
|
||||
- Named gcloud configurations for multi-environment
|
||||
- Version control infrastructure code
|
||||
- Implement automated testing in CI/CD
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Need | Choose |
|
||||
|------|--------|
|
||||
| Sub-50ms latency globally | Cloudflare Workers |
|
||||
| Large file storage (zero egress) | Cloudflare R2 |
|
||||
| SQL database (global reads) | Cloudflare D1 |
|
||||
| Containerized workloads | Docker + Cloud Run/GKE |
|
||||
| Enterprise Kubernetes | GKE |
|
||||
| Managed relational DB | Cloud SQL |
|
||||
| Static site + API | Cloudflare Pages |
|
||||
| WebSocket/real-time | Cloudflare Durable Objects |
|
||||
| ML/AI pipelines | GCP Vertex AI |
|
||||
| Browser automation | Cloudflare Browser Rendering |
|
||||
|
||||
## Resources
|
||||
|
||||
- **Cloudflare Docs:** https://developers.cloudflare.com
|
||||
- **Docker Docs:** https://docs.docker.com
|
||||
- **GCP Docs:** https://cloud.google.com/docs
|
||||
- **Wrangler CLI:** https://developers.cloudflare.com/workers/wrangler/
|
||||
- **gcloud CLI:** https://cloud.google.com/sdk/gcloud
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Cloudflare Workers
|
||||
- [ ] Install Wrangler CLI
|
||||
- [ ] Create Worker project
|
||||
- [ ] Configure wrangler.toml (bindings, routes)
|
||||
- [ ] Test locally with `wrangler dev`
|
||||
- [ ] Deploy with `wrangler deploy`
|
||||
|
||||
### Docker
|
||||
- [ ] Write Dockerfile with multi-stage builds
|
||||
- [ ] Create .dockerignore file
|
||||
- [ ] Test build locally
|
||||
- [ ] Push to registry
|
||||
- [ ] Deploy to target platform
|
||||
|
||||
### Google Cloud
|
||||
- [ ] Install gcloud CLI
|
||||
- [ ] Authenticate with service account
|
||||
- [ ] Create project and enable APIs
|
||||
- [ ] Configure IAM permissions
|
||||
- [ ] Deploy and monitor resources
|
||||
305
skills/devops/references/browser-rendering.md
Normal file
305
skills/devops/references/browser-rendering.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Cloudflare Browser Rendering
|
||||
|
||||
Headless browser automation with Puppeteer/Playwright on Cloudflare Workers.
|
||||
|
||||
## Setup
|
||||
|
||||
**wrangler.toml:**
|
||||
```toml
|
||||
name = "browser-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
browser = { binding = "MYBROWSER" }
|
||||
```
|
||||
|
||||
## Basic Screenshot Worker
|
||||
|
||||
```typescript
|
||||
import puppeteer from '@cloudflare/puppeteer';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
|
||||
const screenshot = await page.screenshot({ type: 'png' });
|
||||
|
||||
await browser.close();
|
||||
|
||||
return new Response(screenshot, {
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Session Reuse (Cost Optimization)
|
||||
|
||||
```typescript
|
||||
// Disconnect instead of close
|
||||
await browser.disconnect();
|
||||
|
||||
// Retrieve and reconnect
|
||||
const sessions = await puppeteer.sessions(env.MYBROWSER);
|
||||
const freeSession = sessions.find(s => !s.connectionId);
|
||||
|
||||
if (freeSession) {
|
||||
const browser = await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
|
||||
}
|
||||
```
|
||||
|
||||
## PDF Generation
|
||||
|
||||
```typescript
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial; padding: 50px; }
|
||||
h1 { color: #2c3e50; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Certificate</h1>
|
||||
<p>Awarded to: <strong>John Doe</strong></p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
const pdf = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
return new Response(pdf, {
|
||||
headers: { 'Content-Type': 'application/pdf' }
|
||||
});
|
||||
```
|
||||
|
||||
## Durable Objects for Persistent Sessions
|
||||
|
||||
```typescript
|
||||
export class Browser {
|
||||
state: DurableObjectState;
|
||||
browser: any;
|
||||
lastUsed: number;
|
||||
|
||||
constructor(state: DurableObjectState, env: Env) {
|
||||
this.state = state;
|
||||
this.lastUsed = Date.now();
|
||||
}
|
||||
|
||||
async fetch(request: Request, env: Env) {
|
||||
if (!this.browser) {
|
||||
this.browser = await puppeteer.launch(env.MYBROWSER);
|
||||
}
|
||||
|
||||
this.lastUsed = Date.now();
|
||||
await this.state.storage.setAlarm(Date.now() + 10000);
|
||||
|
||||
const page = await this.browser.newPage();
|
||||
const url = new URL(request.url).searchParams.get('url');
|
||||
await page.goto(url);
|
||||
const screenshot = await page.screenshot();
|
||||
await page.close();
|
||||
|
||||
return new Response(screenshot, {
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
});
|
||||
}
|
||||
|
||||
async alarm() {
|
||||
if (Date.now() - this.lastUsed > 60000) {
|
||||
await this.browser?.close();
|
||||
this.browser = null;
|
||||
} else {
|
||||
await this.state.storage.setAlarm(Date.now() + 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI-Powered Web Scraper
|
||||
|
||||
```typescript
|
||||
import { Ai } from '@cloudflare/ai';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://news.ycombinator.com');
|
||||
const content = await page.content();
|
||||
await browser.close();
|
||||
|
||||
const ai = new Ai(env.AI);
|
||||
const response = await ai.run('@cf/meta/llama-3-8b-instruct', {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Extract top 5 article titles and URLs as JSON'
|
||||
},
|
||||
{ role: 'user', content: content }
|
||||
]
|
||||
});
|
||||
|
||||
return Response.json(response);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Crawler with Queues
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async queue(batch: MessageBatch<any>, env: Env): Promise<void> {
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
|
||||
for (const message of batch.messages) {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(message.body.url);
|
||||
|
||||
const links = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('a')).map(a => a.href);
|
||||
});
|
||||
|
||||
for (const link of links) {
|
||||
await env.QUEUE.send({ url: link });
|
||||
}
|
||||
|
||||
await page.close();
|
||||
message.ack();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Timeout
|
||||
```typescript
|
||||
await page.goto(url, {
|
||||
timeout: 60000, // 60 seconds max
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
await page.waitForSelector('.content', { timeout: 45000 });
|
||||
```
|
||||
|
||||
### Viewport
|
||||
```typescript
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
```
|
||||
|
||||
### Screenshot Options
|
||||
```typescript
|
||||
const screenshot = await page.screenshot({
|
||||
type: 'png', // 'png' | 'jpeg' | 'webp'
|
||||
quality: 90, // JPEG/WebP only
|
||||
fullPage: true, // Full scrollable page
|
||||
clip: { // Crop
|
||||
x: 0, y: 0,
|
||||
width: 800,
|
||||
height: 600
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Limits & Pricing
|
||||
|
||||
### Free Plan
|
||||
- 10 minutes/day
|
||||
- 3 concurrent browsers
|
||||
- 3 new browsers/minute
|
||||
|
||||
### Paid Plan
|
||||
- 10 hours/month included
|
||||
- 30 concurrent browsers
|
||||
- 30 new browsers/minute
|
||||
- $0.09/hour overage
|
||||
- $2.00/concurrent browser overage
|
||||
|
||||
### Cost Optimization
|
||||
1. Use `disconnect()` instead of `close()`
|
||||
2. Enable Keep-Alive (10 min max)
|
||||
3. Pool tabs with browser contexts
|
||||
4. Cache auth state with KV
|
||||
5. Implement Durable Objects cleanup
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Session Management
|
||||
- Always use `disconnect()` for reuse
|
||||
- Implement session pooling
|
||||
- Track session IDs and states
|
||||
|
||||
### Performance
|
||||
- Cache content in KV
|
||||
- Use browser contexts vs multiple browsers
|
||||
- Choose appropriate `waitUntil` strategy
|
||||
- Set realistic timeouts
|
||||
|
||||
### Error Handling
|
||||
- Handle timeout errors gracefully
|
||||
- Check session availability before connecting
|
||||
- Validate responses before caching
|
||||
|
||||
### Security
|
||||
- Validate user-provided URLs
|
||||
- Implement authentication
|
||||
- Sanitize extracted content
|
||||
- Set appropriate CORS headers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Timeout Errors:**
|
||||
```typescript
|
||||
await page.goto(url, {
|
||||
timeout: 60000,
|
||||
waitUntil: 'domcontentloaded' // Faster than networkidle2
|
||||
});
|
||||
```
|
||||
|
||||
**Memory Issues:**
|
||||
```typescript
|
||||
await page.close(); // Close pages
|
||||
await browser.disconnect(); // Reuse session
|
||||
```
|
||||
|
||||
**Font Rendering:**
|
||||
Use supported fonts (Noto Sans, Roboto, etc.) or inject custom:
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins" rel="stylesheet">
|
||||
```
|
||||
|
||||
## Key Methods
|
||||
|
||||
### Puppeteer
|
||||
- `puppeteer.launch(binding)` - Start browser
|
||||
- `puppeteer.connect(binding, sessionId)` - Reconnect
|
||||
- `puppeteer.sessions(binding)` - List sessions
|
||||
- `browser.newPage()` - Create page
|
||||
- `browser.disconnect()` - Disconnect (keep alive)
|
||||
- `browser.close()` - Close (terminate)
|
||||
- `page.goto(url, options)` - Navigate
|
||||
- `page.screenshot(options)` - Capture
|
||||
- `page.pdf(options)` - Generate PDF
|
||||
- `page.content()` - Get HTML
|
||||
- `page.evaluate(fn)` - Execute JS
|
||||
|
||||
## Resources
|
||||
|
||||
- Docs: https://developers.cloudflare.com/browser-rendering/
|
||||
- Puppeteer: https://pptr.dev/
|
||||
- Examples: https://developers.cloudflare.com/workers/examples/
|
||||
123
skills/devops/references/cloudflare-d1-kv.md
Normal file
123
skills/devops/references/cloudflare-d1-kv.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Cloudflare D1 & KV
|
||||
|
||||
## D1 (SQLite Database)
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Create database
|
||||
wrangler d1 create my-database
|
||||
|
||||
# Add to wrangler.toml
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "my-database"
|
||||
database_id = "YOUR_DATABASE_ID"
|
||||
|
||||
# Apply schema
|
||||
wrangler d1 execute my-database --file=./schema.sql
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
// Query
|
||||
const result = await env.DB.prepare(
|
||||
"SELECT * FROM users WHERE id = ?"
|
||||
).bind(userId).first();
|
||||
|
||||
// Insert
|
||||
await env.DB.prepare(
|
||||
"INSERT INTO users (name, email) VALUES (?, ?)"
|
||||
).bind("Alice", "alice@example.com").run();
|
||||
|
||||
// Batch (atomic)
|
||||
await env.DB.batch([
|
||||
env.DB.prepare("UPDATE accounts SET balance = balance - 100 WHERE id = ?").bind(user1),
|
||||
env.DB.prepare("UPDATE accounts SET balance = balance + 100 WHERE id = ?").bind(user2)
|
||||
]);
|
||||
|
||||
// All results
|
||||
const { results } = await env.DB.prepare("SELECT * FROM users").all();
|
||||
```
|
||||
|
||||
### Features
|
||||
- Global read replication (low-latency reads)
|
||||
- Single-writer consistency
|
||||
- Standard SQLite syntax
|
||||
- 25GB database size limit
|
||||
- ACID transactions with batch
|
||||
|
||||
## KV (Key-Value Store)
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Create namespace
|
||||
wrangler kv:namespace create MY_KV
|
||||
|
||||
# Add to wrangler.toml
|
||||
[[kv_namespaces]]
|
||||
binding = "KV"
|
||||
id = "YOUR_NAMESPACE_ID"
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
// Put with TTL
|
||||
await env.KV.put("session:token", JSON.stringify(data), {
|
||||
expirationTtl: 3600,
|
||||
metadata: { userId: "123" }
|
||||
});
|
||||
|
||||
// Get
|
||||
const value = await env.KV.get("session:token");
|
||||
const json = await env.KV.get("session:token", "json");
|
||||
const buffer = await env.KV.get("session:token", "arrayBuffer");
|
||||
const stream = await env.KV.get("session:token", "stream");
|
||||
|
||||
// Get with metadata
|
||||
const { value, metadata } = await env.KV.getWithMetadata("session:token");
|
||||
|
||||
// Delete
|
||||
await env.KV.delete("session:token");
|
||||
|
||||
// List
|
||||
const list = await env.KV.list({ prefix: "user:" });
|
||||
```
|
||||
|
||||
### Features
|
||||
- Sub-millisecond reads (edge-cached)
|
||||
- Eventual consistency (~60 seconds globally)
|
||||
- 25MB value size limit
|
||||
- Automatic expiration (TTL)
|
||||
|
||||
## Use Cases
|
||||
|
||||
### D1
|
||||
- Relational data
|
||||
- Complex queries with JOINs
|
||||
- ACID transactions
|
||||
- User accounts, orders, inventory
|
||||
|
||||
### KV
|
||||
- Cache
|
||||
- Sessions
|
||||
- Feature flags
|
||||
- Rate limiting
|
||||
- Real-time counters
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Need | Choose |
|
||||
|------|--------|
|
||||
| SQL queries | D1 |
|
||||
| Sub-millisecond reads | KV |
|
||||
| ACID transactions | D1 |
|
||||
| Large values (>25MB) | R2 |
|
||||
| Strong consistency | D1 (writes), Durable Objects |
|
||||
| Automatic expiration | KV |
|
||||
|
||||
## Resources
|
||||
|
||||
- D1: https://developers.cloudflare.com/d1/
|
||||
- KV: https://developers.cloudflare.com/kv/
|
||||
271
skills/devops/references/cloudflare-platform.md
Normal file
271
skills/devops/references/cloudflare-platform.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Cloudflare Platform Overview
|
||||
|
||||
Cloudflare Developer Platform: comprehensive edge computing ecosystem for full-stack applications on global network across 300+ cities.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Edge Computing Model
|
||||
|
||||
**Global Network:**
|
||||
- Code runs on servers in 300+ cities globally
|
||||
- Requests execute from nearest location
|
||||
- Ultra-low latency (<50ms typical)
|
||||
- Automatic failover and redundancy
|
||||
|
||||
**V8 Isolates:**
|
||||
- Lightweight execution environments (faster than containers)
|
||||
- Millisecond cold starts
|
||||
- Zero infrastructure management
|
||||
- Automatic scaling
|
||||
- Pay-per-request pricing
|
||||
|
||||
### Key Components
|
||||
|
||||
**Workers** - Serverless functions on edge
|
||||
- HTTP/scheduled/queue/email handlers
|
||||
- JavaScript/TypeScript/Python/Rust support
|
||||
- Max 50ms CPU (free), 30s (paid)
|
||||
- 128MB memory limit
|
||||
|
||||
**D1** - SQLite database with global read replication
|
||||
- Standard SQLite syntax
|
||||
- Single-writer consistency
|
||||
- Global read replication
|
||||
- 25GB database size limit
|
||||
- Batch operations for transactions
|
||||
|
||||
**KV** - Distributed key-value store
|
||||
- Sub-millisecond reads (edge-cached)
|
||||
- Eventual consistency (~60s globally)
|
||||
- 25MB value size limit
|
||||
- Automatic TTL expiration
|
||||
- Best for: cache, sessions, feature flags
|
||||
|
||||
**R2** - Object storage (S3-compatible)
|
||||
- Zero egress fees (huge cost advantage)
|
||||
- Unlimited storage
|
||||
- 5TB object size limit
|
||||
- S3-compatible API
|
||||
- Multipart upload support
|
||||
|
||||
**Durable Objects** - Stateful compute with WebSockets
|
||||
- Single-instance coordination (strong consistency)
|
||||
- Persistent storage (1GB limit paid)
|
||||
- WebSocket support
|
||||
- Automatic hibernation
|
||||
|
||||
**Queues** - Message queue system
|
||||
- At-least-once delivery
|
||||
- Automatic retries (exponential backoff)
|
||||
- Dead-letter queue support
|
||||
- Batch processing
|
||||
|
||||
**Pages** - Static site hosting + serverless functions
|
||||
- Git integration (auto-deploy)
|
||||
- Directory-based routing
|
||||
- Framework support (Next.js, Remix, Astro, SvelteKit)
|
||||
- Built-in preview deployments
|
||||
|
||||
**Workers AI** - Run AI models on edge
|
||||
- LLMs (Llama 3, Mistral, Gemma, Qwen)
|
||||
- Image generation (Stable Diffusion, DALL-E)
|
||||
- Embeddings (BGE, GTE)
|
||||
- Speech recognition (Whisper)
|
||||
- No GPU management required
|
||||
|
||||
**Browser Rendering** - Headless browser automation
|
||||
- Puppeteer/Playwright support
|
||||
- Screenshots, PDFs, web scraping
|
||||
- Session reuse for cost optimization
|
||||
- MCP server support for AI agents
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Full-Stack Application
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Cloudflare Pages (Frontend) │
|
||||
│ Next.js / Remix / Astro │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────▼──────────────────────┐
|
||||
│ Workers (API Layer) │
|
||||
│ - Routing │
|
||||
│ - Authentication │
|
||||
│ - Business logic │
|
||||
└─┬──────┬──────┬──────┬──────┬───────────┘
|
||||
│ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────────────┐
|
||||
│ D1 │ │ KV │ │ R2 │ │ DO │ │ Workers AI │
|
||||
└────┘ └────┘ └────┘ └────┘ └────────────┘
|
||||
```
|
||||
|
||||
### Polyglot Storage Pattern
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
// KV: Fast cache
|
||||
const cached = await env.KV.get(key);
|
||||
if (cached) return new Response(cached);
|
||||
|
||||
// D1: Structured data
|
||||
const user = await env.DB.prepare(
|
||||
"SELECT * FROM users WHERE id = ?"
|
||||
).bind(userId).first();
|
||||
|
||||
// R2: Media files
|
||||
const avatar = await env.R2_BUCKET.get(`avatars/${user.id}.jpg`);
|
||||
|
||||
// Durable Objects: Real-time
|
||||
const chat = env.CHAT_ROOM.get(env.CHAT_ROOM.idFromName(roomId));
|
||||
|
||||
// Queue: Async processing
|
||||
await env.EMAIL_QUEUE.send({ to: user.email, template: 'welcome' });
|
||||
|
||||
return new Response(JSON.stringify({ user }));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Wrangler CLI Essentials
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install -g wrangler
|
||||
wrangler login
|
||||
wrangler init my-worker
|
||||
```
|
||||
|
||||
### Core Commands
|
||||
```bash
|
||||
# Development
|
||||
wrangler dev # Local dev server
|
||||
wrangler dev --remote # Dev on real edge
|
||||
|
||||
# Deployment
|
||||
wrangler deploy # Deploy to production
|
||||
wrangler deploy --dry-run # Preview changes
|
||||
|
||||
# Logs
|
||||
wrangler tail # Real-time logs
|
||||
wrangler tail --format pretty # Formatted logs
|
||||
|
||||
# Versions
|
||||
wrangler deployments list # List deployments
|
||||
wrangler rollback [version] # Rollback
|
||||
|
||||
# Secrets
|
||||
wrangler secret put SECRET_NAME
|
||||
wrangler secret list
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
```bash
|
||||
# D1
|
||||
wrangler d1 create my-db
|
||||
wrangler d1 execute my-db --file=schema.sql
|
||||
|
||||
# KV
|
||||
wrangler kv:namespace create MY_KV
|
||||
wrangler kv:key put --binding=MY_KV "key" "value"
|
||||
|
||||
# R2
|
||||
wrangler r2 bucket create my-bucket
|
||||
wrangler r2 object put my-bucket/file.txt --file=./file.txt
|
||||
```
|
||||
|
||||
## Configuration (wrangler.toml)
|
||||
|
||||
```toml
|
||||
name = "my-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
# Environment variables
|
||||
[vars]
|
||||
ENVIRONMENT = "production"
|
||||
|
||||
# D1 Database
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "my-database"
|
||||
database_id = "YOUR_DATABASE_ID"
|
||||
|
||||
# KV Namespace
|
||||
[[kv_namespaces]]
|
||||
binding = "KV"
|
||||
id = "YOUR_NAMESPACE_ID"
|
||||
|
||||
# R2 Bucket
|
||||
[[r2_buckets]]
|
||||
binding = "R2_BUCKET"
|
||||
bucket_name = "my-bucket"
|
||||
|
||||
# Durable Objects
|
||||
[[durable_objects.bindings]]
|
||||
name = "COUNTER"
|
||||
class_name = "Counter"
|
||||
script_name = "my-worker"
|
||||
|
||||
# Queues
|
||||
[[queues.producers]]
|
||||
binding = "MY_QUEUE"
|
||||
queue = "my-queue"
|
||||
|
||||
# Workers AI
|
||||
[ai]
|
||||
binding = "AI"
|
||||
|
||||
# Cron triggers
|
||||
[triggers]
|
||||
crons = ["0 0 * * *"]
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance
|
||||
- Keep Workers lightweight (<1MB bundled)
|
||||
- Use bindings over fetch (faster than HTTP)
|
||||
- Leverage KV and Cache API for frequently accessed data
|
||||
- Use D1 batch for multiple queries
|
||||
- Stream large responses
|
||||
|
||||
### Security
|
||||
- Use `wrangler secret` for API keys
|
||||
- Separate production/staging/development environments
|
||||
- Validate user input
|
||||
- Implement rate limiting (KV or Durable Objects)
|
||||
- Configure proper CORS headers
|
||||
|
||||
### Cost Optimization
|
||||
- R2 for large files (zero egress fees vs S3)
|
||||
- KV for caching (reduce D1/R2 requests)
|
||||
- Request deduplication with caching
|
||||
- Efficient D1 queries (proper indexing)
|
||||
- Monitor usage via Cloudflare Analytics
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Need | Choose |
|
||||
|------|--------|
|
||||
| Sub-millisecond reads | KV |
|
||||
| SQL queries | D1 |
|
||||
| Large files (>25MB) | R2 |
|
||||
| Real-time WebSockets | Durable Objects |
|
||||
| Async background jobs | Queues |
|
||||
| ACID transactions | D1 |
|
||||
| Strong consistency | Durable Objects |
|
||||
| Zero egress costs | R2 |
|
||||
| AI inference | Workers AI |
|
||||
| Static site hosting | Pages |
|
||||
|
||||
## Resources
|
||||
|
||||
- Docs: https://developers.cloudflare.com
|
||||
- Wrangler: https://developers.cloudflare.com/workers/wrangler/
|
||||
- Discord: https://discord.cloudflare.com
|
||||
- Examples: https://developers.cloudflare.com/workers/examples/
|
||||
- Status: https://www.cloudflarestatus.com
|
||||
280
skills/devops/references/cloudflare-r2-storage.md
Normal file
280
skills/devops/references/cloudflare-r2-storage.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Cloudflare R2 Storage
|
||||
|
||||
S3-compatible object storage with zero egress fees.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Create Bucket
|
||||
```bash
|
||||
wrangler r2 bucket create my-bucket
|
||||
wrangler r2 bucket create my-bucket --location=wnam
|
||||
```
|
||||
|
||||
Locations: `wnam`, `enam`, `weur`, `eeur`, `apac`
|
||||
|
||||
### Upload Object
|
||||
```bash
|
||||
wrangler r2 object put my-bucket/file.txt --file=./local-file.txt
|
||||
```
|
||||
|
||||
### Workers Binding
|
||||
|
||||
**wrangler.toml:**
|
||||
```toml
|
||||
[[r2_buckets]]
|
||||
binding = "MY_BUCKET"
|
||||
bucket_name = "my-bucket"
|
||||
```
|
||||
|
||||
**Worker:**
|
||||
```typescript
|
||||
// Put
|
||||
await env.MY_BUCKET.put('user-uploads/photo.jpg', imageData, {
|
||||
httpMetadata: {
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: 'public, max-age=31536000'
|
||||
},
|
||||
customMetadata: {
|
||||
uploadedBy: userId,
|
||||
uploadDate: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
// Get
|
||||
const object = await env.MY_BUCKET.get('large-file.mp4');
|
||||
if (!object) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(object.body, {
|
||||
headers: {
|
||||
'Content-Type': object.httpMetadata.contentType,
|
||||
'ETag': object.etag
|
||||
}
|
||||
});
|
||||
|
||||
// List
|
||||
const listed = await env.MY_BUCKET.list({
|
||||
prefix: 'user-uploads/',
|
||||
limit: 100
|
||||
});
|
||||
|
||||
// Delete
|
||||
await env.MY_BUCKET.delete('old-file.txt');
|
||||
|
||||
// Head (check existence)
|
||||
const object = await env.MY_BUCKET.head('file.txt');
|
||||
if (object) {
|
||||
console.log('Size:', object.size);
|
||||
}
|
||||
```
|
||||
|
||||
## S3 API Integration
|
||||
|
||||
### AWS CLI
|
||||
```bash
|
||||
# Configure
|
||||
aws configure
|
||||
# Access Key ID: <your-key-id>
|
||||
# Secret Access Key: <your-secret>
|
||||
# Region: auto
|
||||
|
||||
# Operations
|
||||
aws s3api list-buckets --endpoint-url https://<accountid>.r2.cloudflarestorage.com
|
||||
|
||||
aws s3 cp file.txt s3://my-bucket/ --endpoint-url https://<accountid>.r2.cloudflarestorage.com
|
||||
|
||||
# Presigned URL
|
||||
aws s3 presign s3://my-bucket/file.txt --endpoint-url https://<accountid>.r2.cloudflarestorage.com --expires-in 3600
|
||||
```
|
||||
|
||||
### JavaScript (AWS SDK v3)
|
||||
```javascript
|
||||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
const s3 = new S3Client({
|
||||
region: "auto",
|
||||
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY
|
||||
}
|
||||
});
|
||||
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: "my-bucket",
|
||||
Key: "file.txt",
|
||||
Body: fileContents
|
||||
}));
|
||||
```
|
||||
|
||||
### Python (Boto3)
|
||||
```python
|
||||
import boto3
|
||||
|
||||
s3 = boto3.client(
|
||||
service_name='s3',
|
||||
endpoint_url=f'https://{account_id}.r2.cloudflarestorage.com',
|
||||
aws_access_key_id=access_key_id,
|
||||
aws_secret_access_key=secret_access_key,
|
||||
region_name='auto'
|
||||
)
|
||||
|
||||
s3.upload_fileobj(file_obj, 'my-bucket', 'file.txt')
|
||||
s3.download_file('my-bucket', 'file.txt', './local-file.txt')
|
||||
```
|
||||
|
||||
## Multipart Uploads
|
||||
|
||||
For files >100MB:
|
||||
|
||||
```typescript
|
||||
const multipart = await env.MY_BUCKET.createMultipartUpload('large-file.mp4');
|
||||
|
||||
// Upload parts (5MiB - 5GiB each, max 10,000 parts)
|
||||
const part1 = await multipart.uploadPart(1, chunk1);
|
||||
const part2 = await multipart.uploadPart(2, chunk2);
|
||||
|
||||
// Complete
|
||||
const object = await multipart.complete([part1, part2]);
|
||||
```
|
||||
|
||||
### Rclone (Large Files)
|
||||
```bash
|
||||
rclone config # Configure Cloudflare R2
|
||||
|
||||
# Upload with optimization
|
||||
rclone copy large-video.mp4 r2:my-bucket/ \
|
||||
--s3-upload-cutoff=100M \
|
||||
--s3-chunk-size=100M
|
||||
```
|
||||
|
||||
## Public Buckets
|
||||
|
||||
### Enable Public Access
|
||||
1. Dashboard → R2 → Bucket → Settings → Public Access
|
||||
2. Add custom domain (recommended) or use r2.dev
|
||||
|
||||
**r2.dev (rate-limited):**
|
||||
```
|
||||
https://pub-<hash>.r2.dev/file.txt
|
||||
```
|
||||
|
||||
**Custom domain (production):**
|
||||
Cloudflare handles DNS/TLS automatically
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
```bash
|
||||
wrangler r2 bucket cors put my-bucket --rules '[
|
||||
{
|
||||
"AllowedOrigins": ["https://example.com"],
|
||||
"AllowedMethods": ["GET", "PUT", "POST"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"ExposeHeaders": ["ETag"],
|
||||
"MaxAgeSeconds": 3600
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
## Lifecycle Rules
|
||||
|
||||
```bash
|
||||
wrangler r2 bucket lifecycle put my-bucket --rules '[
|
||||
{
|
||||
"action": {"type": "AbortIncompleteMultipartUpload"},
|
||||
"filter": {},
|
||||
"abortIncompleteMultipartUploadDays": 7
|
||||
},
|
||||
{
|
||||
"action": {"type": "Transition", "storageClass": "InfrequentAccess"},
|
||||
"filter": {"prefix": "archives/"},
|
||||
"daysFromCreation": 90
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
## Event Notifications
|
||||
|
||||
```bash
|
||||
wrangler r2 bucket notification create my-bucket \
|
||||
--queue=my-queue \
|
||||
--event-type=object-create
|
||||
```
|
||||
|
||||
Supported events: `object-create`, `object-delete`
|
||||
|
||||
## Data Migration
|
||||
|
||||
### Sippy (Incremental)
|
||||
```bash
|
||||
wrangler r2 bucket sippy enable my-bucket \
|
||||
--provider=aws \
|
||||
--bucket=source-bucket \
|
||||
--region=us-east-1 \
|
||||
--access-key-id=$AWS_KEY \
|
||||
--secret-access-key=$AWS_SECRET
|
||||
```
|
||||
|
||||
Objects migrate on first request.
|
||||
|
||||
### Super Slurper (Bulk)
|
||||
Use dashboard for one-time complete migration from AWS, GCS, Azure.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance
|
||||
- Use Cloudflare Cache with custom domains
|
||||
- Multipart uploads for files >100MB
|
||||
- Rclone for batch operations
|
||||
- Location hints match user geography
|
||||
|
||||
### Security
|
||||
- Never commit Access Keys
|
||||
- Use environment variables
|
||||
- Bucket-scoped tokens for least privilege
|
||||
- Presigned URLs for temporary access
|
||||
- Enable Cloudflare Access for protection
|
||||
|
||||
### Cost Optimization
|
||||
- Infrequent Access storage for archives (30+ days)
|
||||
- Lifecycle rules to auto-transition/delete
|
||||
- Larger multipart chunks = fewer Class A operations
|
||||
- Monitor usage via dashboard
|
||||
|
||||
### Naming
|
||||
- Bucket names: lowercase, hyphens, 3-63 chars
|
||||
- Avoid sequential prefixes (use hashed for performance)
|
||||
- No dots in bucket names if using custom domains with TLS
|
||||
|
||||
## Limits
|
||||
|
||||
- Buckets per account: 1,000
|
||||
- Object size: 5TB max
|
||||
- Lifecycle rules: 1,000 per bucket
|
||||
- Event notification rules: 100 per bucket
|
||||
- r2.dev rate limit: 1,000 req/min (use custom domains)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**401 Unauthorized:**
|
||||
- Verify Access Keys
|
||||
- Check endpoint URL includes account ID
|
||||
- Ensure region is "auto"
|
||||
|
||||
**403 Forbidden:**
|
||||
- Check bucket permissions
|
||||
- Verify CORS configuration
|
||||
- Confirm bucket exists
|
||||
|
||||
**Presigned URLs not working:**
|
||||
- Verify CORS configuration
|
||||
- Check URL expiry time
|
||||
- Ensure origin matches CORS rules
|
||||
|
||||
## Resources
|
||||
|
||||
- Docs: https://developers.cloudflare.com/r2/
|
||||
- Wrangler: https://developers.cloudflare.com/r2/reference/wrangler-commands/
|
||||
- S3 Compatibility: https://developers.cloudflare.com/r2/api/s3/api/
|
||||
- Workers API: https://developers.cloudflare.com/r2/api/workers/
|
||||
312
skills/devops/references/cloudflare-workers-advanced.md
Normal file
312
skills/devops/references/cloudflare-workers-advanced.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Cloudflare Workers Advanced Patterns
|
||||
|
||||
Advanced techniques for optimization, performance, and complex workflows.
|
||||
|
||||
## Session Reuse and Connection Pooling
|
||||
|
||||
### Durable Objects for Persistent Sessions
|
||||
```typescript
|
||||
export class Browser {
|
||||
state: DurableObjectState;
|
||||
browser: any;
|
||||
lastUsed: number;
|
||||
|
||||
constructor(state: DurableObjectState, env: Env) {
|
||||
this.state = state;
|
||||
this.lastUsed = Date.now();
|
||||
}
|
||||
|
||||
async fetch(request: Request, env: Env) {
|
||||
if (!this.browser) {
|
||||
this.browser = await puppeteer.launch(env.MYBROWSER);
|
||||
}
|
||||
|
||||
this.lastUsed = Date.now();
|
||||
await this.state.storage.setAlarm(Date.now() + 10000);
|
||||
|
||||
const page = await this.browser.newPage();
|
||||
await page.goto(new URL(request.url).searchParams.get('url'));
|
||||
const screenshot = await page.screenshot();
|
||||
await page.close();
|
||||
|
||||
return new Response(screenshot);
|
||||
}
|
||||
|
||||
async alarm() {
|
||||
if (Date.now() - this.lastUsed > 60000) {
|
||||
await this.browser?.close();
|
||||
this.browser = null;
|
||||
} else {
|
||||
await this.state.storage.setAlarm(Date.now() + 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Tier Caching Strategy
|
||||
|
||||
```typescript
|
||||
const CACHE_TTL = 3600;
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const cache = caches.default;
|
||||
const cacheKey = new Request(request.url);
|
||||
|
||||
// 1. Check edge cache
|
||||
let response = await cache.match(cacheKey);
|
||||
if (response) return response;
|
||||
|
||||
// 2. Check KV cache
|
||||
const kvCached = await env.MY_KV.get(request.url);
|
||||
if (kvCached) {
|
||||
response = new Response(kvCached);
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return response;
|
||||
}
|
||||
|
||||
// 3. Fetch from origin
|
||||
response = await fetch(request);
|
||||
|
||||
// 4. Store in both caches
|
||||
ctx.waitUntil(Promise.all([
|
||||
cache.put(cacheKey, response.clone()),
|
||||
env.MY_KV.put(request.url, await response.clone().text(), {
|
||||
expirationTtl: CACHE_TTL
|
||||
})
|
||||
]));
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## WebSocket with Durable Objects
|
||||
|
||||
```typescript
|
||||
export class ChatRoom {
|
||||
state: DurableObjectState;
|
||||
sessions: Set<WebSocket>;
|
||||
|
||||
constructor(state: DurableObjectState) {
|
||||
this.state = state;
|
||||
this.sessions = new Set();
|
||||
}
|
||||
|
||||
async fetch(request: Request) {
|
||||
const pair = new WebSocketPair();
|
||||
const [client, server] = Object.values(pair);
|
||||
|
||||
this.state.acceptWebSocket(server);
|
||||
this.sessions.add(server);
|
||||
|
||||
return new Response(null, { status: 101, webSocket: client });
|
||||
}
|
||||
|
||||
async webSocketMessage(ws: WebSocket, message: string) {
|
||||
// Broadcast to all connected clients
|
||||
for (const session of this.sessions) {
|
||||
session.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
async webSocketClose(ws: WebSocket) {
|
||||
this.sessions.delete(ws);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Queue-Based Crawler
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async queue(batch: MessageBatch<any>, env: Env): Promise<void> {
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
|
||||
for (const message of batch.messages) {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(message.body.url);
|
||||
|
||||
// Extract links
|
||||
const links = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('a'))
|
||||
.map(a => a.href);
|
||||
});
|
||||
|
||||
// Queue new links
|
||||
for (const link of links) {
|
||||
await env.QUEUE.send({ url: link });
|
||||
}
|
||||
|
||||
await page.close();
|
||||
message.ack();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Authentication Pattern
|
||||
|
||||
```typescript
|
||||
import { sign, verify } from 'hono/jwt';
|
||||
|
||||
async function authenticate(request: Request, env: Env): Promise<any> {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const payload = await verify(token, env.JWT_SECRET);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
try {
|
||||
const user = await authenticate(request, env);
|
||||
return new Response(`Hello ${user.name}`);
|
||||
} catch (error) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Code Splitting
|
||||
|
||||
```typescript
|
||||
// Lazy load large dependencies
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === '/heavy') {
|
||||
const { processHeavy } = await import('./heavy');
|
||||
return processHeavy(request);
|
||||
}
|
||||
|
||||
return new Response('OK');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Batch Operations with D1
|
||||
|
||||
```typescript
|
||||
// Efficient bulk inserts
|
||||
const statements = users.map(user =>
|
||||
env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
|
||||
.bind(user.name, user.email)
|
||||
);
|
||||
|
||||
await env.DB.batch(statements);
|
||||
```
|
||||
|
||||
## Stream Processing
|
||||
|
||||
```typescript
|
||||
const { readable, writable } = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
// Process chunk
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
response.body.pipeTo(writable);
|
||||
return new Response(readable);
|
||||
```
|
||||
|
||||
## AI-Powered Web Scraper
|
||||
|
||||
```typescript
|
||||
import { Ai } from '@cloudflare/ai';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Render page
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://news.ycombinator.com');
|
||||
const content = await page.content();
|
||||
await browser.close();
|
||||
|
||||
// Extract with AI
|
||||
const ai = new Ai(env.AI);
|
||||
const response = await ai.run('@cf/meta/llama-3-8b-instruct', {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Extract top 5 article titles and URLs as JSON array'
|
||||
},
|
||||
{ role: 'user', content: content }
|
||||
]
|
||||
});
|
||||
|
||||
return Response.json(response);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Bundle Size
|
||||
- Keep Workers <1MB bundled
|
||||
- Remove unused dependencies
|
||||
- Use code splitting
|
||||
- Check with: `wrangler deploy --dry-run --outdir=dist`
|
||||
|
||||
### Cold Starts
|
||||
- Minimize initialization code
|
||||
- Use bindings over fetch
|
||||
- Avoid large imports at top level
|
||||
|
||||
### Memory Management
|
||||
- Close pages when done: `await page.close()`
|
||||
- Disconnect browsers: `await browser.disconnect()`
|
||||
- Implement cleanup alarms in Durable Objects
|
||||
|
||||
### Request Optimization
|
||||
- Use server-side filtering with `--filter`
|
||||
- Batch operations with D1 `.batch()`
|
||||
- Stream large responses
|
||||
- Implement proper caching
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
```bash
|
||||
# Real-time logs
|
||||
wrangler tail --format pretty
|
||||
|
||||
# Filter by status
|
||||
wrangler tail --status error
|
||||
|
||||
# Check deployments
|
||||
wrangler deployments list
|
||||
|
||||
# Rollback
|
||||
wrangler rollback [version-id]
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Multi-stage error handling implemented
|
||||
- [ ] Rate limiting configured
|
||||
- [ ] Caching strategy in place
|
||||
- [ ] Secrets managed with `wrangler secret`
|
||||
- [ ] Health checks implemented
|
||||
- [ ] Monitoring alerts configured
|
||||
- [ ] Session reuse for browser rendering
|
||||
- [ ] Resource cleanup (pages, browsers)
|
||||
- [ ] Proper timeout configurations
|
||||
- [ ] CI/CD pipeline set up
|
||||
|
||||
## Resources
|
||||
|
||||
- Advanced Patterns: https://developers.cloudflare.com/workers/examples/
|
||||
- Durable Objects: https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
|
||||
- Performance: https://developers.cloudflare.com/workers/platform/limits/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user