This commit is contained in:
admin
2026-01-30 03:04:10 +00:00
parent bcc4d242c4
commit 2a3dedde11
1218 changed files with 214731 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/coreyhaines31/marketingskills/tree/main/skills/ab-test-setup",
"type": "github-subdir",
"installed_at": "2026-01-30T02:22:07.322363226Z",
"repo_url": "https://github.com/coreyhaines31/marketingskills.git",
"subdir": "skills/ab-test-setup",
"version": "a04cb61"
}

265
ab-test-setup/SKILL.md Normal file
View File

@@ -0,0 +1,265 @@
---
name: ab-test-setup
version: 1.0.0
description: When the user wants to plan, design, or implement an A/B test or experiment. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," or "hypothesis." For tracking implementation, see analytics-tracking.
---
# A/B Test Setup
You are an expert in experimentation and A/B testing. Your goal is to help design tests that produce statistically valid, actionable results.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before designing a test, understand:
1. **Test Context** - What are you trying to improve? What change are you considering?
2. **Current State** - Baseline conversion rate? Current traffic volume?
3. **Constraints** - Technical complexity? Timeline? Tools available?
---
## Core Principles
### 1. Start with a Hypothesis
- Not just "let's see what happens"
- Specific prediction of outcome
- Based on reasoning or data
### 2. Test One Thing
- Single variable per test
- Otherwise you don't know what worked
### 3. Statistical Rigor
- Pre-determine sample size
- Don't peek and stop early
- Commit to the methodology
### 4. Measure What Matters
- Primary metric tied to business value
- Secondary metrics for context
- Guardrail metrics to prevent harm
---
## Hypothesis Framework
### Structure
```
Because [observation/data],
we believe [change]
will cause [expected outcome]
for [audience].
We'll know this is true when [metrics].
```
### Example
**Weak**: "Changing the button color might increase clicks."
**Strong**: "Because users report difficulty finding the CTA (per heatmaps and feedback), we believe making the button larger and using contrasting color will increase CTA clicks by 15%+ for new visitors. We'll measure click-through rate from page view to signup start."
---
## Test Types
| Type | Description | Traffic Needed |
|------|-------------|----------------|
| A/B | Two versions, single change | Moderate |
| A/B/n | Multiple variants | Higher |
| MVT | Multiple changes in combinations | Very high |
| Split URL | Different URLs for variants | Moderate |
---
## Sample Size
### Quick Reference
| Baseline | 10% Lift | 20% Lift | 50% Lift |
|----------|----------|----------|----------|
| 1% | 150k/variant | 39k/variant | 6k/variant |
| 3% | 47k/variant | 12k/variant | 2k/variant |
| 5% | 27k/variant | 7k/variant | 1.2k/variant |
| 10% | 12k/variant | 3k/variant | 550/variant |
**Calculators:**
- [Evan Miller's](https://www.evanmiller.org/ab-testing/sample-size.html)
- [Optimizely's](https://www.optimizely.com/sample-size-calculator/)
**For detailed sample size tables and duration calculations**: See [references/sample-size-guide.md](references/sample-size-guide.md)
---
## Metrics Selection
### Primary Metric
- Single metric that matters most
- Directly tied to hypothesis
- What you'll use to call the test
### Secondary Metrics
- Support primary metric interpretation
- Explain why/how the change worked
### Guardrail Metrics
- Things that shouldn't get worse
- Stop test if significantly negative
### Example: Pricing Page Test
- **Primary**: Plan selection rate
- **Secondary**: Time on page, plan distribution
- **Guardrail**: Support tickets, refund rate
---
## Designing Variants
### What to Vary
| Category | Examples |
|----------|----------|
| Headlines/Copy | Message angle, value prop, specificity, tone |
| Visual Design | Layout, color, images, hierarchy |
| CTA | Button copy, size, placement, number |
| Content | Information included, order, amount, social proof |
### Best Practices
- Single, meaningful change
- Bold enough to make a difference
- True to the hypothesis
---
## Traffic Allocation
| Approach | Split | When to Use |
|----------|-------|-------------|
| Standard | 50/50 | Default for A/B |
| Conservative | 90/10, 80/20 | Limit risk of bad variant |
| Ramping | Start small, increase | Technical risk mitigation |
**Considerations:**
- Consistency: Users see same variant on return
- Balanced exposure across time of day/week
---
## Implementation
### Client-Side
- JavaScript modifies page after load
- Quick to implement, can cause flicker
- Tools: PostHog, Optimizely, VWO
### Server-Side
- Variant determined before render
- No flicker, requires dev work
- Tools: PostHog, LaunchDarkly, Split
---
## Running the Test
### Pre-Launch Checklist
- [ ] Hypothesis documented
- [ ] Primary metric defined
- [ ] Sample size calculated
- [ ] Variants implemented correctly
- [ ] Tracking verified
- [ ] QA completed on all variants
### During the Test
**DO:**
- Monitor for technical issues
- Check segment quality
- Document external factors
**DON'T:**
- Peek at results and stop early
- Make changes to variants
- Add traffic from new sources
### The Peeking Problem
Looking at results before reaching sample size and stopping early leads to false positives and wrong decisions. Pre-commit to sample size and trust the process.
---
## Analyzing Results
### Statistical Significance
- 95% confidence = p-value < 0.05
- Means <5% chance result is random
- Not a guarantee—just a threshold
### Analysis Checklist
1. **Reach sample size?** If not, result is preliminary
2. **Statistically significant?** Check confidence intervals
3. **Effect size meaningful?** Compare to MDE, project impact
4. **Secondary metrics consistent?** Support the primary?
5. **Guardrail concerns?** Anything get worse?
6. **Segment differences?** Mobile vs. desktop? New vs. returning?
### Interpreting Results
| Result | Conclusion |
|--------|------------|
| Significant winner | Implement variant |
| Significant loser | Keep control, learn why |
| No significant difference | Need more traffic or bolder test |
| Mixed signals | Dig deeper, maybe segment |
---
## Documentation
Document every test with:
- Hypothesis
- Variants (with screenshots)
- Results (sample, metrics, significance)
- Decision and learnings
**For templates**: See [references/test-templates.md](references/test-templates.md)
---
## Common Mistakes
### Test Design
- Testing too small a change (undetectable)
- Testing too many things (can't isolate)
- No clear hypothesis
### Execution
- Stopping early
- Changing things mid-test
- Not checking implementation
### Analysis
- Ignoring confidence intervals
- Cherry-picking segments
- Over-interpreting inconclusive results
---
## Task-Specific Questions
1. What's your current conversion rate?
2. How much traffic does this page get?
3. What change are you considering and why?
4. What's the smallest improvement worth detecting?
5. What tools do you have for testing?
6. Have you tested this area before?
---
## Related Skills
- **page-cro**: For generating test ideas based on CRO principles
- **analytics-tracking**: For setting up test measurement
- **copywriting**: For creating variant copy

View File

@@ -0,0 +1,252 @@
# Sample Size Guide
Reference for calculating sample sizes and test duration.
## Sample Size Fundamentals
### Required Inputs
1. **Baseline conversion rate**: Your current rate
2. **Minimum detectable effect (MDE)**: Smallest change worth detecting
3. **Statistical significance level**: Usually 95% (α = 0.05)
4. **Statistical power**: Usually 80% (β = 0.20)
### What These Mean
**Baseline conversion rate**: If your page converts at 5%, that's your baseline.
**MDE (Minimum Detectable Effect)**: The smallest improvement you care about detecting. Set this based on:
- Business impact (is a 5% lift meaningful?)
- Implementation cost (worth the effort?)
- Realistic expectations (what have past tests shown?)
**Statistical significance (95%)**: Means there's less than 5% chance the observed difference is due to random chance.
**Statistical power (80%)**: Means if there's a real effect of size MDE, you have 80% chance of detecting it.
---
## Sample Size Quick Reference Tables
### Conversion Rate: 1%
| Lift to Detect | Sample per Variant | Total Sample |
|----------------|-------------------|--------------|
| 5% (1% → 1.05%) | 1,500,000 | 3,000,000 |
| 10% (1% → 1.1%) | 380,000 | 760,000 |
| 20% (1% → 1.2%) | 97,000 | 194,000 |
| 50% (1% → 1.5%) | 16,000 | 32,000 |
| 100% (1% → 2%) | 4,200 | 8,400 |
### Conversion Rate: 3%
| Lift to Detect | Sample per Variant | Total Sample |
|----------------|-------------------|--------------|
| 5% (3% → 3.15%) | 480,000 | 960,000 |
| 10% (3% → 3.3%) | 120,000 | 240,000 |
| 20% (3% → 3.6%) | 31,000 | 62,000 |
| 50% (3% → 4.5%) | 5,200 | 10,400 |
| 100% (3% → 6%) | 1,400 | 2,800 |
### Conversion Rate: 5%
| Lift to Detect | Sample per Variant | Total Sample |
|----------------|-------------------|--------------|
| 5% (5% → 5.25%) | 280,000 | 560,000 |
| 10% (5% → 5.5%) | 72,000 | 144,000 |
| 20% (5% → 6%) | 18,000 | 36,000 |
| 50% (5% → 7.5%) | 3,100 | 6,200 |
| 100% (5% → 10%) | 810 | 1,620 |
### Conversion Rate: 10%
| Lift to Detect | Sample per Variant | Total Sample |
|----------------|-------------------|--------------|
| 5% (10% → 10.5%) | 130,000 | 260,000 |
| 10% (10% → 11%) | 34,000 | 68,000 |
| 20% (10% → 12%) | 8,700 | 17,400 |
| 50% (10% → 15%) | 1,500 | 3,000 |
| 100% (10% → 20%) | 400 | 800 |
### Conversion Rate: 20%
| Lift to Detect | Sample per Variant | Total Sample |
|----------------|-------------------|--------------|
| 5% (20% → 21%) | 60,000 | 120,000 |
| 10% (20% → 22%) | 16,000 | 32,000 |
| 20% (20% → 24%) | 4,000 | 8,000 |
| 50% (20% → 30%) | 700 | 1,400 |
| 100% (20% → 40%) | 200 | 400 |
---
## Duration Calculator
### Formula
```
Duration (days) = (Sample per variant × Number of variants) / (Daily traffic × % exposed)
```
### Examples
**Scenario 1: High-traffic page**
- Need: 10,000 per variant (2 variants = 20,000 total)
- Daily traffic: 5,000 visitors
- 100% exposed to test
- Duration: 20,000 / 5,000 = **4 days**
**Scenario 2: Medium-traffic page**
- Need: 30,000 per variant (60,000 total)
- Daily traffic: 2,000 visitors
- 100% exposed
- Duration: 60,000 / 2,000 = **30 days**
**Scenario 3: Low-traffic with partial exposure**
- Need: 15,000 per variant (30,000 total)
- Daily traffic: 500 visitors
- 50% exposed to test
- Effective daily: 250
- Duration: 30,000 / 250 = **120 days** (too long!)
### Minimum Duration Rules
Even with sufficient sample size, run tests for at least:
- **1 full week**: To capture day-of-week variation
- **2 business cycles**: If B2B (weekday vs. weekend patterns)
- **Through paydays**: If e-commerce (beginning/end of month)
### Maximum Duration Guidelines
Avoid running tests longer than 4-8 weeks:
- Novelty effects wear off
- External factors intervene
- Opportunity cost of other tests
---
## Online Calculators
### Recommended Tools
**Evan Miller's Calculator**
https://www.evanmiller.org/ab-testing/sample-size.html
- Simple interface
- Bookmark-worthy
**Optimizely's Calculator**
https://www.optimizely.com/sample-size-calculator/
- Business-friendly language
- Duration estimates
**AB Test Guide Calculator**
https://www.abtestguide.com/calc/
- Includes Bayesian option
- Multiple test types
**VWO Duration Calculator**
https://vwo.com/tools/ab-test-duration-calculator/
- Duration-focused
- Good for planning
---
## Adjusting for Multiple Variants
With more than 2 variants (A/B/n tests), you need more sample:
| Variants | Multiplier |
|----------|------------|
| 2 (A/B) | 1x |
| 3 (A/B/C) | ~1.5x |
| 4 (A/B/C/D) | ~2x |
| 5+ | Consider reducing variants |
**Why?** More comparisons increase chance of false positives. You're comparing:
- A vs B
- A vs C
- B vs C (sometimes)
Apply Bonferroni correction or use tools that handle this automatically.
---
## Common Sample Size Mistakes
### 1. Underpowered tests
**Problem**: Not enough sample to detect realistic effects
**Fix**: Be realistic about MDE, get more traffic, or don't test
### 2. Overpowered tests
**Problem**: Waiting for sample size when you already have significance
**Fix**: This is actually fine—you committed to sample size, honor it
### 3. Wrong baseline rate
**Problem**: Using wrong conversion rate for calculation
**Fix**: Use the specific metric and page, not site-wide averages
### 4. Ignoring segments
**Problem**: Calculating for full traffic, then analyzing segments
**Fix**: If you plan segment analysis, calculate sample for smallest segment
### 5. Testing too many things
**Problem**: Dividing traffic too many ways
**Fix**: Prioritize ruthlessly, run fewer concurrent tests
---
## When Sample Size Requirements Are Too High
Options when you can't get enough traffic:
1. **Increase MDE**: Accept only detecting larger effects (20%+ lift)
2. **Lower confidence**: Use 90% instead of 95% (risky, document it)
3. **Reduce variants**: Test only the most promising variant
4. **Combine traffic**: Test across multiple similar pages
5. **Test upstream**: Test earlier in funnel where traffic is higher
6. **Don't test**: Make decision based on qualitative data instead
7. **Longer test**: Accept longer duration (weeks/months)
---
## Sequential Testing
If you must check results before reaching sample size:
### What is it?
Statistical method that adjusts for multiple looks at data.
### When to use
- High-risk changes
- Need to stop bad variants early
- Time-sensitive decisions
### Tools that support it
- Optimizely (Stats Accelerator)
- VWO (SmartStats)
- PostHog (Bayesian approach)
### Tradeoff
- More flexibility to stop early
- Slightly larger sample size requirement
- More complex analysis
---
## Quick Decision Framework
### Can I run this test?
```
Daily traffic to page: _____
Baseline conversion rate: _____
MDE I care about: _____
Sample needed per variant: _____ (from tables above)
Days to run: Sample / Daily traffic = _____
If days > 60: Consider alternatives
If days > 30: Acceptable for high-impact tests
If days < 14: Likely feasible
If days < 7: Easy to run, consider running longer anyway
```

View File

@@ -0,0 +1,268 @@
# A/B Test Templates Reference
Templates for planning, documenting, and analyzing experiments.
## Test Plan Template
```markdown
# A/B Test: [Name]
## Overview
- **Owner**: [Name]
- **Test ID**: [ID in testing tool]
- **Page/Feature**: [What's being tested]
- **Planned dates**: [Start] - [End]
## Hypothesis
Because [observation/data],
we believe [change]
will cause [expected outcome]
for [audience].
We'll know this is true when [metrics].
## Test Design
| Element | Details |
|---------|---------|
| Test type | A/B / A/B/n / MVT |
| Duration | X weeks |
| Sample size | X per variant |
| Traffic allocation | 50/50 |
| Tool | [Tool name] |
| Implementation | Client-side / Server-side |
## Variants
### Control (A)
[Screenshot]
- Current experience
- [Key details about current state]
### Variant (B)
[Screenshot or mockup]
- [Specific change #1]
- [Specific change #2]
- Rationale: [Why we think this will win]
## Metrics
### Primary
- **Metric**: [metric name]
- **Definition**: [how it's calculated]
- **Current baseline**: [X%]
- **Minimum detectable effect**: [X%]
### Secondary
- [Metric 1]: [what it tells us]
- [Metric 2]: [what it tells us]
- [Metric 3]: [what it tells us]
### Guardrails
- [Metric that shouldn't get worse]
- [Another safety metric]
## Segment Analysis Plan
- Mobile vs. desktop
- New vs. returning visitors
- Traffic source
- [Other relevant segments]
## Success Criteria
- Winner: [Primary metric improves by X% with 95% confidence]
- Loser: [Primary metric decreases significantly]
- Inconclusive: [What we'll do if no significant result]
## Pre-Launch Checklist
- [ ] Hypothesis documented and reviewed
- [ ] Primary metric defined and trackable
- [ ] Sample size calculated
- [ ] Test duration estimated
- [ ] Variants implemented correctly
- [ ] Tracking verified in all variants
- [ ] QA completed on all variants
- [ ] Stakeholders informed
- [ ] Calendar hold for analysis date
```
---
## Results Documentation Template
```markdown
# A/B Test Results: [Name]
## Summary
| Element | Value |
|---------|-------|
| Test ID | [ID] |
| Dates | [Start] - [End] |
| Duration | X days |
| Result | Winner / Loser / Inconclusive |
| Decision | [What we're doing] |
## Hypothesis (Reminder)
[Copy from test plan]
## Results
### Sample Size
| Variant | Target | Actual | % of target |
|---------|--------|--------|-------------|
| Control | X | Y | Z% |
| Variant | X | Y | Z% |
### Primary Metric: [Metric Name]
| Variant | Value | 95% CI | vs. Control |
|---------|-------|--------|-------------|
| Control | X% | [X%, Y%] | — |
| Variant | X% | [X%, Y%] | +X% |
**Statistical significance**: p = X.XX (95% = sig / not sig)
**Practical significance**: [Is this lift meaningful for the business?]
### Secondary Metrics
| Metric | Control | Variant | Change | Significant? |
|--------|---------|---------|--------|--------------|
| [Metric 1] | X | Y | +Z% | Yes/No |
| [Metric 2] | X | Y | +Z% | Yes/No |
### Guardrail Metrics
| Metric | Control | Variant | Change | Concern? |
|--------|---------|---------|--------|----------|
| [Metric 1] | X | Y | +Z% | Yes/No |
### Segment Analysis
**Mobile vs. Desktop**
| Segment | Control | Variant | Lift |
|---------|---------|---------|------|
| Mobile | X% | Y% | +Z% |
| Desktop | X% | Y% | +Z% |
**New vs. Returning**
| Segment | Control | Variant | Lift |
|---------|---------|---------|------|
| New | X% | Y% | +Z% |
| Returning | X% | Y% | +Z% |
## Interpretation
### What happened?
[Explanation of results in plain language]
### Why do we think this happened?
[Analysis and reasoning]
### Caveats
[Any limitations, external factors, or concerns]
## Decision
**Winner**: [Control / Variant]
**Action**: [Implement variant / Keep control / Re-test]
**Timeline**: [When changes will be implemented]
## Learnings
### What we learned
- [Key insight 1]
- [Key insight 2]
### What to test next
- [Follow-up test idea 1]
- [Follow-up test idea 2]
### Impact
- **Projected lift**: [X% improvement in Y metric]
- **Business impact**: [Revenue, conversions, etc.]
```
---
## Test Repository Entry Template
For tracking all tests in a central location:
```markdown
| Test ID | Name | Page | Dates | Primary Metric | Result | Lift | Link |
|---------|------|------|-------|----------------|--------|------|------|
| 001 | Hero headline test | Homepage | 1/1-1/15 | CTR | Winner | +12% | [Link] |
| 002 | Pricing table layout | Pricing | 1/10-1/31 | Plan selection | Loser | -5% | [Link] |
| 003 | Signup form fields | Signup | 2/1-2/14 | Completion | Inconclusive | +2% | [Link] |
```
---
## Quick Test Brief Template
For simple tests that don't need full documentation:
```markdown
## [Test Name]
**What**: [One sentence description]
**Why**: [One sentence hypothesis]
**Metric**: [Primary metric]
**Duration**: [X weeks]
**Result**: [TBD / Winner / Loser / Inconclusive]
**Learnings**: [Key takeaway]
```
---
## Stakeholder Update Template
```markdown
## A/B Test Update: [Name]
**Status**: Running / Complete
**Days remaining**: X (or complete)
**Current sample**: X% of target
### Preliminary observations
[What we're seeing - without making decisions yet]
### Next steps
[What happens next]
### Timeline
- [Date]: Analysis complete
- [Date]: Decision and recommendation
- [Date]: Implementation (if winner)
```
---
## Experiment Prioritization Scorecard
For deciding which tests to run:
| Factor | Weight | Test A | Test B | Test C |
|--------|--------|--------|--------|--------|
| Potential impact | 30% | | | |
| Confidence in hypothesis | 25% | | | |
| Ease of implementation | 20% | | | |
| Risk if wrong | 15% | | | |
| Strategic alignment | 10% | | | |
| **Total** | | | | |
Scoring: 1-5 (5 = best)
---
## Hypothesis Bank Template
For collecting test ideas:
```markdown
| ID | Page/Area | Observation | Hypothesis | Potential Impact | Status |
|----|-----------|-------------|------------|------------------|--------|
| H1 | Homepage | Low scroll depth | Shorter hero will increase scroll | High | Testing |
| H2 | Pricing | Users compare plans | Comparison table will help | Medium | Backlog |
| H3 | Signup | Drop-off at email | Social login will increase completion | Medium | Backlog |
```

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/getsentry/skills/tree/main/plugins/sentry-skills/skills/agents-md",
"type": "github-subdir",
"installed_at": "2026-01-30T02:23:31.680827073Z",
"repo_url": "https://github.com/getsentry/skills.git",
"subdir": "plugins/sentry-skills/skills/agents-md",
"version": "bb366a0"
}

111
agents-md/SKILL.md Normal file
View File

@@ -0,0 +1,111 @@
---
name: agents-md
description: This skill should be used when the user asks to "create AGENTS.md", "update AGENTS.md", "maintain agent docs", "set up CLAUDE.md", or needs to keep agent instructions concise. Guides discovery of local skills and enforces minimal documentation style.
---
# Maintaining AGENTS.md
AGENTS.md is the canonical agent-facing documentation. Keep it minimal—agents are capable and don't need hand-holding.
## File Setup
1. Create `AGENTS.md` at project root
2. Create symlink: `ln -s AGENTS.md CLAUDE.md`
## Before Writing
Discover local skills to reference:
```bash
find .claude/skills -name "SKILL.md" 2>/dev/null
ls plugins/*/skills/*/SKILL.md 2>/dev/null
```
Read each skill's frontmatter to understand when to reference it.
## Writing Rules
- **Headers + bullets** - No paragraphs
- **Code blocks** - For commands and templates
- **Reference, don't duplicate** - Point to skills: "Use `db-migrate` skill. See `.claude/skills/db-migrate/SKILL.md`"
- **No filler** - No intros, conclusions, or pleasantries
- **Trust capabilities** - Omit obvious context
## Required Sections
### Package Manager
Which tool and key commands only:
```markdown
## Package Manager
Use **pnpm**: `pnpm install`, `pnpm dev`, `pnpm test`
```
### Commit Attribution
Always include this section. Agents should use their own identity:
```markdown
## Commit Attribution
AI commits MUST include:
```
Co-Authored-By: (the agent model's name and attribution byline)
```
Example: `Co-Authored-By: Claude Sonnet 4 <noreply@example.com>`
```
### Key Conventions
Project-specific patterns agents must follow. Keep brief.
### Local Skills
Reference each discovered skill:
```markdown
## Database
Use `db-migrate` skill for schema changes. See `.claude/skills/db-migrate/SKILL.md`
## Testing
Use `write-tests` skill. See `.claude/skills/write-tests/SKILL.md`
```
## Optional Sections
Add only if truly needed:
- API route patterns (show template, not explanation)
- CLI commands (table format)
- File naming conventions
## Anti-Patterns
Omit these:
- "Welcome to..." or "This document explains..."
- "You should..." or "Remember to..."
- Content duplicated from skills (reference instead)
- Obvious instructions ("run tests", "write clean code")
- Explanations of why (just say what)
- Long prose paragraphs
## Example Structure
```markdown
# Agent Instructions
## Package Manager
Use **pnpm**: `pnpm install`, `pnpm dev`
## Commit Attribution
AI commits MUST include:
```
Co-Authored-By: (the agent model's name and attribution byline)
```
## API Routes
[Template code block]
## Database
Use `db-migrate` skill. See `.claude/skills/db-migrate/SKILL.md`
## Testing
Use `write-tests` skill. See `.claude/skills/write-tests/SKILL.md`
## CLI
| Command | Description |
|---------|-------------|
| `pnpm cli sync` | Sync data |
```

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/cloudflare/skills/tree/main/skills/agents-sdk",
"type": "github-subdir",
"installed_at": "2026-01-30T02:29:59.573967009Z",
"repo_url": "https://github.com/cloudflare/skills.git",
"subdir": "skills/agents-sdk",
"version": "75a603b"
}

160
agents-sdk/SKILL.md Normal file
View File

@@ -0,0 +1,160 @@
---
name: agents-sdk
description: Build stateful AI agents using the Cloudflare Agents SDK. Load when creating agents with persistent state, scheduling, RPC, MCP servers, email handling, or streaming chat. Covers Agent class, AIChatAgent, state management, and Code Mode for reduced token usage.
---
# Cloudflare Agents SDK
Build persistent, stateful AI agents on Cloudflare Workers using the `agents` npm package.
## FIRST: Verify Installation
```bash
npm install agents
```
Agents require a binding in `wrangler.jsonc`:
```jsonc
{
"durable_objects": {
// "class_name" must match your Agent class name exactly
"bindings": [{ "name": "Counter", "class_name": "Counter" }]
},
"migrations": [
// Required: list all Agent classes for SQLite storage
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
]
}
```
## Choosing an Agent Type
| Use Case | Base Class | Package |
|----------|------------|---------|
| Custom state + RPC, no chat | `Agent` | `agents` |
| Chat with message persistence | `AIChatAgent` | `@cloudflare/ai-chat` |
| Building an MCP server | `McpAgent` | `agents/mcp` |
## Key Concepts
- **Agent** base class provides state, scheduling, RPC, MCP, and email capabilities
- **AIChatAgent** adds streaming chat with automatic message persistence and resumable streams
- **Code Mode** generates executable code instead of tool calls—reduces token usage significantly
- **this.state / this.setState()** - automatic persistence to SQLite, broadcasts to clients
- **this.schedule()** - schedule tasks at Date, delay (seconds), or cron expression
- **@callable** decorator - expose methods to clients via WebSocket RPC
## Quick Reference
| Task | API |
|------|-----|
| Persist state | `this.setState({ count: 1 })` |
| Read state | `this.state.count` |
| Schedule task | `this.schedule(60, "taskMethod", payload)` |
| Schedule cron | `this.schedule("0 * * * *", "hourlyTask")` |
| Cancel schedule | `this.cancelSchedule(id)` |
| Queue task | `this.queue("processItem", payload)` |
| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` |
| RPC method | `@callable() async myMethod() { ... }` |
| Streaming RPC | `@callable({ streaming: true }) async stream(res) { ... }` |
## Minimal Agent
```typescript
import { Agent, routeAgentRequest, callable } from "agents";
type State = { count: number };
export class Counter extends Agent<Env, State> {
initialState = { count: 0 };
@callable()
increment() {
this.setState({ count: this.state.count + 1 });
return this.state.count;
}
}
export default {
fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 })
};
```
## Streaming Chat Agent
Use `AIChatAgent` for chat with automatic message persistence and resumable streaming.
**Install additional dependencies first:**
```bash
npm install @cloudflare/ai-chat ai @ai-sdk/openai
```
**Add wrangler.jsonc config** (same pattern as base Agent):
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "Chat", "class_name": "Chat" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Chat"] }]
}
```
```typescript
import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
onFinish
});
return result.toUIMessageStreamResponse();
}
}
export default {
fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 })
};
```
**Client** (React):
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Chat", name: "my-chat" });
const { messages, input, handleSubmit } = useAgentChat({ agent });
```
## Detailed References
- **[references/state-scheduling.md](references/state-scheduling.md)** - State persistence, scheduling, queues
- **[references/streaming-chat.md](references/streaming-chat.md)** - AIChatAgent, resumable streams, UI patterns
- **[references/codemode.md](references/codemode.md)** - Generate code instead of tool calls (token savings)
- **[references/mcp.md](references/mcp.md)** - MCP server integration
- **[references/email.md](references/email.md)** - Email routing and handling
## When to Use Code Mode
Code Mode generates executable JavaScript instead of making individual tool calls. Use it when:
- Chaining multiple tool calls in sequence
- Complex conditional logic across tools
- MCP server orchestration (multiple servers)
- Token budget is constrained
See [references/codemode.md](references/codemode.md) for setup and examples.
## Best Practices
1. **Prefer streaming**: Use `streamText` and `toUIMessageStreamResponse()` for chat
2. **Use AIChatAgent for chat**: Handles message persistence and resumable streams automatically
3. **Type your state**: `Agent<Env, State>` ensures type safety for `this.state`
4. **Use @callable for RPC**: Cleaner than manual WebSocket message handling
5. **Code Mode for complex workflows**: Reduces round-trips and token usage
6. **Schedule vs Queue**: Use `schedule()` for time-based, `queue()` for sequential processing

View File

@@ -0,0 +1,207 @@
# Code Mode (Experimental)
Code Mode generates executable JavaScript instead of making individual tool calls. This significantly reduces token usage and enables complex multi-tool workflows.
## Why Code Mode?
Traditional tool calling:
- One tool call per LLM request
- Multiple round-trips for chained operations
- High token usage for complex workflows
Code Mode:
- LLM generates code that orchestrates multiple tools
- Single execution for complex workflows
- Self-debugging and error recovery
- Ideal for MCP server orchestration
## Setup
### 1. Wrangler Config
```jsonc
{
"name": "my-agent-worker",
"compatibility_flags": ["experimental", "enable_ctx_exports"],
"durable_objects": {
// "class_name" must match your Agent class name exactly
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [
// Required: list all Agent classes for SQLite storage
{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }
],
"services": [
{
"binding": "globalOutbound",
// "service" must match "name" above
"service": "my-agent-worker",
"entrypoint": "globalOutbound"
},
{
"binding": "CodeModeProxy",
"service": "my-agent-worker",
"entrypoint": "CodeModeProxy"
}
],
"worker_loaders": [{ "binding": "LOADER" }]
}
```
### 2. Export Required Classes
```typescript
// Export the proxy for tool execution (required for codemode)
export { CodeModeProxy } from "@cloudflare/codemode/ai";
// Define outbound fetch handler for security filtering
export const globalOutbound = {
fetch: async (input: string | URL | RequestInfo, init?: RequestInit) => {
const url = new URL(
typeof input === "string"
? input
: typeof input === "object" && "url" in input
? input.url
: input.toString()
);
// Block certain domains if needed
if (url.hostname === "blocked.example.com") {
return new Response("Not allowed", { status: 403 });
}
return fetch(input, init);
}
};
```
### 3. Install Dependencies
```bash
npm install @cloudflare/codemode ai @ai-sdk/openai zod
```
### 4. Use Code Mode in Agent
```typescript
import { Agent } from "agents";
import { experimental_codemode as codemode } from "@cloudflare/codemode/ai";
import { streamText, tool, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import { env } from "cloudflare:workers";
import { z } from "zod";
const tools = {
getWeather: tool({
description: "Get weather for a location",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => `Weather: ${location} 72°F`
}),
sendEmail: tool({
description: "Send an email",
parameters: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
execute: async ({ to, subject, body }) => `Email sent to ${to}`
})
};
export class MyAgent extends Agent<Env, State> {
tools = {};
// Method called by codemode proxy
callTool(functionName: string, args: unknown[]) {
return this.tools[functionName]?.execute?.(args, {
abortSignal: new AbortController().signal,
toolCallId: "codemode",
messages: []
});
}
async onChatMessage() {
this.tools = { ...tools, ...this.mcp.getAITools() };
const { prompt, tools: wrappedTools } = await codemode({
prompt: "You are a helpful assistant...",
tools: this.tools,
globalOutbound: env.globalOutbound,
loader: env.LOADER,
proxy: this.ctx.exports.CodeModeProxy({
props: {
binding: "MyAgent", // Class name
name: this.name, // Instance name
callback: "callTool" // Method to call
}
})
});
const result = streamText({
system: prompt,
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.state.messages),
tools: wrappedTools // Use wrapped tools, not original
});
// ... handle stream
}
}
```
## Generated Code Example
When user asks "Check the weather in NYC and email me the forecast", codemode generates:
```javascript
async function executeTask() {
const weather = await codemode.getWeather({ location: "NYC" });
await codemode.sendEmail({
to: "user@example.com",
subject: "NYC Weather Forecast",
body: `Current weather: ${weather}`
});
return { success: true, weather };
}
```
## MCP Server Orchestration
Code Mode excels at orchestrating multiple MCP servers:
```javascript
async function executeTask() {
// Query file system MCP
const files = await codemode.listFiles({ path: "/projects" });
// Query database MCP
const status = await codemode.queryDatabase({
query: "SELECT * FROM projects WHERE name = ?",
params: [files[0].name]
});
// Conditional logic based on results
if (status.length === 0) {
await codemode.createTask({
title: `Review: ${files[0].name}`,
priority: "high"
});
}
return { files, status };
}
```
## When to Use
| Scenario | Use Code Mode? |
|----------|---------------|
| Single tool call | No |
| Chained tool calls | Yes |
| Conditional logic across tools | Yes |
| MCP multi-server workflows | Yes |
| Token budget constrained | Yes |
| Simple Q&A chat | No |
## Limitations
- Experimental - API may change
- Requires Cloudflare Workers
- JavaScript execution only (Python planned)
- Requires additional wrangler config

View File

@@ -0,0 +1,119 @@
# Email Handling
Agents can receive and reply to emails via Cloudflare Email Routing.
## Wrangler Config
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "EmailAgent", "class_name": "EmailAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailAgent"] }],
"send_email": [
{ "name": "SEB", "destination_address": "reply@yourdomain.com" }
]
}
```
Configure Email Routing in Cloudflare dashboard to forward to your Worker.
## Implement onEmail
```typescript
import { Agent, AgentEmail } from "agents";
import PostalMime from "postal-mime";
type State = { emails: Array<{ from: string; subject: string; text: string; timestamp: Date }> };
export class EmailAgent extends Agent<Env, State> {
initialState: State = { emails: [] };
async onEmail(email: AgentEmail) {
console.log("From:", email.from);
console.log("To:", email.to);
console.log("Subject:", email.headers.get("subject"));
// Get raw email content
const raw = await email.getRaw();
// Parse with postal-mime
const parsed = await PostalMime.parse(raw);
// Update state
this.setState({
emails: [...this.state.emails, {
from: email.from,
subject: parsed.subject ?? "",
text: parsed.text ?? "",
timestamp: new Date()
}]
});
// Reply
await this.replyToEmail(email, {
fromName: "My Agent",
subject: `Re: ${email.headers.get("subject")}`,
body: "Thanks for your email! I'll process it shortly.",
contentType: "text/plain"
});
}
}
```
**Install postal-mime for parsing:**
```bash
npm install postal-mime
```
## Route Emails to Agent
```typescript
import { routeAgentRequest, routeAgentEmail, createAddressBasedEmailResolver } from "agents";
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent")
});
},
async fetch(request, env) {
return routeAgentRequest(request, env) ?? new Response("Not found", { status: 404 });
}
};
```
## Custom Email Resolvers
### Header-Based Resolver
Routes based on X-Agent headers in replies:
```typescript
import { createHeaderBasedEmailResolver } from "agents";
await routeAgentEmail(message, env, {
resolver: createHeaderBasedEmailResolver()
});
```
### Custom Resolver
```typescript
const customResolver = async (email, env) => {
// Parse recipient to determine agent
const [localPart] = email.to.split("@");
if (localPart.startsWith("support-")) {
return {
agentName: "SupportAgent",
agentId: localPart.replace("support-", "")
};
}
return null; // Don't route
};
await routeAgentEmail(message, env, { resolver: customResolver });
```

View File

@@ -0,0 +1,153 @@
# MCP Server Integration
Agents include a multi-server MCP client for connecting to external MCP servers.
## Add an MCP Server
```typescript
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable()
async addServer(name: string, url: string) {
const result = await this.addMcpServer(
name,
url,
"https://my-worker.workers.dev", // callback host for OAuth
"agents" // routing prefix
);
if (result.state === "authenticating") {
// OAuth required - redirect user to result.authUrl
return { needsAuth: true, authUrl: result.authUrl };
}
return { ready: true, id: result.id };
}
}
```
## Use MCP Tools
```typescript
async onChatMessage() {
// Get AI-compatible tools from all connected MCP servers
const mcpTools = this.mcp.getAITools();
const allTools = {
...localTools,
...mcpTools
};
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools: allTools
});
return result.toUIMessageStreamResponse();
}
```
## List MCP Resources
```typescript
// List all registered servers
const servers = this.mcp.listServers();
// List tools from all servers
const tools = this.mcp.listTools();
// List resources
const resources = this.mcp.listResources();
// List prompts
const prompts = this.mcp.listPrompts();
```
## Remove Server
```typescript
await this.removeMcpServer(serverId);
```
## Building an MCP Server
Use `McpAgent` from the SDK to create an MCP server.
**Install dependencies:**
```bash
npm install @modelcontextprotocol/sdk zod
```
**Wrangler config:**
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "MyMCP", "class_name": "MyMCP" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyMCP"] }]
}
```
**Server implementation:**
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { z } from "zod";
type State = { counter: number };
export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "MyMCPServer",
version: "1.0.0"
});
initialState = { counter: 0 };
async init() {
// Register a resource
this.server.resource("counter", "mcp://resource/counter", (uri) => ({
contents: [{ text: String(this.state.counter), uri: uri.href }]
}));
// Register a tool
this.server.registerTool(
"increment",
{
description: "Increment the counter",
inputSchema: { amount: z.number().default(1) }
},
async ({ amount }) => {
this.setState({ counter: this.state.counter + amount });
return {
content: [{ text: `Counter: ${this.state.counter}`, type: "text" }]
};
}
);
}
}
```
## Serve MCP Server
```typescript
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
// SSE transport (legacy)
if (url.pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse", { binding: "MyMCP" }).fetch(request, env, ctx);
}
// Streamable HTTP transport (recommended)
if (url.pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp", { binding: "MyMCP" }).fetch(request, env, ctx);
}
return new Response("Not found", { status: 404 });
}
};
```

View File

@@ -0,0 +1,208 @@
# State, Scheduling & Queues
## State Management
State persists automatically to SQLite and broadcasts to connected clients.
### Define Typed State
```typescript
type State = {
count: number;
items: string[];
lastUpdated: Date;
};
export class MyAgent extends Agent<Env, State> {
initialState: State = {
count: 0,
items: [],
lastUpdated: new Date()
};
}
```
### Read and Update State
```typescript
// Read (lazy-loaded from SQLite on first access)
const count = this.state.count;
// Update (persists to SQLite, broadcasts to clients)
this.setState({
...this.state,
count: this.state.count + 1
});
```
### React to State Changes
```typescript
onStateUpdate(state: State, source: Connection | "server") {
if (source !== "server") {
// Client updated state via WebSocket
console.log("Client update:", state);
}
}
```
### Client-Side State Sync (React)
```tsx
import { useAgent } from "agents/react";
import { useState } from "react";
function App() {
const [state, setLocalState] = useState<State>({ count: 0 });
const agent = useAgent<State>({
agent: "MyAgent",
name: "instance-1",
onStateUpdate: (newState) => setLocalState(newState)
});
const increment = () => {
agent.setState({ ...state, count: state.count + 1 });
};
return <button onClick={increment}>Count: {state.count}</button>;
}
```
The `onStateUpdate` callback receives state changes from the server. Use local React state to store and render the synced state.
## Scheduling
Schedule methods to run at specific times using `this.schedule()`.
### Schedule Types
```typescript
// At specific Date
await this.schedule(new Date("2025-12-25T00:00:00Z"), "sendGreeting", { to: "user" });
// Delay in seconds
await this.schedule(60, "checkStatus", { id: "abc123" }); // 1 minute
// Cron expression (recurring)
await this.schedule("0 * * * *", "hourlyCleanup", {}); // Every hour
await this.schedule("0 9 * * 1-5", "weekdayReport", {}); // 9am weekdays
```
### Schedule Handler
```typescript
export class MyAgent extends Agent<Env, State> {
async sendGreeting(payload: { to: string }, schedule: Schedule) {
console.log(`Sending greeting to ${payload.to}`);
// Cron schedules automatically reschedule; one-time schedules are deleted
}
}
```
### Manage Schedules
```typescript
// Get all schedules
const schedules = this.getSchedules();
// Get by type
const crons = this.getSchedules({ type: "cron" });
// Get by time range
const upcoming = this.getSchedules({
timeRange: { start: new Date(), end: nextWeek }
});
// Cancel
await this.cancelSchedule(schedule.id);
```
## Task Queue
Process tasks sequentially with automatic dequeue on success.
### Queue a Task
```typescript
await this.queue("processItem", { itemId: "123", priority: "high" });
```
### Queue Handler
```typescript
async processItem(payload: { itemId: string }, queueItem: QueueItem) {
const item = await fetchItem(payload.itemId);
await processItem(item);
// Task automatically dequeued on success
}
```
### Queue Operations
```typescript
// Manual dequeue
await this.dequeue(queueItem.id);
// Dequeue all
await this.dequeueAll();
// Dequeue by callback
await this.dequeueAllByCallback("processItem");
// Query queue
const pending = await this.getQueues("priority", "high");
```
## SQL API
Direct SQLite access for custom queries:
```typescript
// Create table
this.sql`
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
name TEXT,
created_at INTEGER DEFAULT (unixepoch())
)
`;
// Insert with params
this.sql`INSERT INTO items (id, name) VALUES (${id}, ${name})`;
// Query with types
const items = this.sql<{ id: string; name: string }>`
SELECT * FROM items WHERE name LIKE ${`%${search}%`}
`;
```
## Lifecycle Callbacks
```typescript
export class MyAgent extends Agent<Env, State> {
// Called when agent starts (after hibernation or first create)
async onStart() {
console.log("Agent started:", this.name);
}
// WebSocket connected
onConnect(conn: Connection, ctx: ConnectionContext) {
console.log("Client connected:", conn.id);
}
// WebSocket message (non-RPC)
onMessage(conn: Connection, message: WSMessage) {
console.log("Received:", message);
}
// State changed
onStateUpdate(state: State, source: Connection | "server") {}
// Error handler
onError(error: unknown) {
console.error("Agent error:", error);
throw error; // Re-throw to propagate
}
}
```

View File

@@ -0,0 +1,176 @@
# Streaming Chat with AIChatAgent
`AIChatAgent` provides streaming chat with automatic message persistence and resumable streams.
## Basic Chat Agent
```typescript
import { AIChatAgent } from "@cloudflare/ai-chat";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## With Custom System Prompt
```typescript
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant specializing in...",
messages: await convertToModelMessages(this.messages),
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## With Tools
```typescript
import { tool } from "ai";
import { z } from "zod";
const tools = {
getWeather: tool({
description: "Get weather for a location",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`
})
};
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## Custom UI Message Stream
For more control, use `createUIMessageStream`:
```typescript
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
onFinish
});
writer.merge(result.toUIMessageStream());
}
});
return createUIMessageStreamResponse({ stream });
}
}
```
## Resumable Streaming
Streams automatically resume if client disconnects and reconnects:
1. Chunks buffered to SQLite during streaming
2. On reconnect, buffered chunks sent immediately
3. Live streaming continues from where it left off
**Enabled by default.** To disable:
```tsx
const { messages } = useAgentChat({ agent, resume: false });
```
## React Client
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function ChatUI() {
const agent = useAgent({
agent: "Chat",
name: "my-chat-session"
});
const {
messages,
input,
handleInputChange,
handleSubmit,
status
} = useAgentChat({ agent });
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
disabled={status === "streaming"}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
```
## Streaming RPC Methods
For non-chat streaming, use `@callable({ streaming: true })`:
```typescript
import { Agent, callable, StreamingResponse } from "agents";
export class MyAgent extends Agent<Env> {
@callable({ streaming: true })
async streamData(stream: StreamingResponse, query: string) {
for (let i = 0; i < 10; i++) {
stream.send(`Result ${i}: ${query}`);
await sleep(100);
}
stream.close();
}
}
```
Client receives streamed messages via WebSocket RPC.
## Status Values
`useAgentChat` status:
| Status | Meaning |
|--------|---------|
| `ready` | Idle, ready for input |
| `streaming` | Response streaming |
| `submitted` | Request sent, waiting |
| `error` | Error occurred |

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/anthropics/skills/tree/main/skills/algorithmic-art",
"type": "github-subdir",
"installed_at": "2026-01-30T02:17:16.089806916Z",
"repo_url": "https://github.com/anthropics/skills.git",
"subdir": "skills/algorithmic-art",
"version": "69c0b1a"
}

202
algorithmic-art/LICENSE.txt Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

405
algorithmic-art/SKILL.md Normal file
View File

@@ -0,0 +1,405 @@
---
name: algorithmic-art
description: Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
license: Complete terms in LICENSE.txt
---
Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms).
This happens in two steps:
1. Algorithmic Philosophy Creation (.md file)
2. Express by creating p5.js generative art (.html + .js files)
First, undertake this task:
## ALGORITHMIC PHILOSOPHY CREATION
To begin, create an ALGORITHMIC PHILOSOPHY (not static images or templates) that will be interpreted through:
- Computational processes, emergent behavior, mathematical beauty
- Seeded randomness, noise fields, organic systems
- Particles, flows, fields, forces
- Parametric variation and controlled chaos
### THE CRITICAL UNDERSTANDING
- What is received: Some subtle input or instructions by the user to take into account, but use as a foundation; it should not constrain creative freedom.
- What is created: An algorithmic philosophy/generative aesthetic movement.
- What happens next: The same version receives the philosophy and EXPRESSES IT IN CODE - creating p5.js sketches that are 90% algorithmic generation, 10% essential parameters.
Consider this approach:
- Write a manifesto for a generative art movement
- The next phase involves writing the algorithm that brings it to life
The philosophy must emphasize: Algorithmic expression. Emergent behavior. Computational beauty. Seeded variation.
### HOW TO GENERATE AN ALGORITHMIC PHILOSOPHY
**Name the movement** (1-2 words): "Organic Turbulence" / "Quantum Harmonics" / "Emergent Stillness"
**Articulate the philosophy** (4-6 paragraphs - concise but complete):
To capture the ALGORITHMIC essence, express how this philosophy manifests through:
- Computational processes and mathematical relationships?
- Noise functions and randomness patterns?
- Particle behaviors and field dynamics?
- Temporal evolution and system states?
- Parametric variation and emergent complexity?
**CRITICAL GUIDELINES:**
- **Avoid redundancy**: Each algorithmic aspect should be mentioned once. Avoid repeating concepts about noise theory, particle dynamics, or mathematical principles unless adding new depth.
- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final algorithm should appear as though it took countless hours to develop, was refined with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted algorithm," "the product of deep computational expertise," "painstaking optimization," "master-level implementation."
- **Leave creative space**: Be specific about the algorithmic direction, but concise enough that the next Claude has room to make interpretive implementation choices at an extremely high level of craftsmanship.
The philosophy must guide the next version to express ideas ALGORITHMICALLY, not through static images. Beauty lives in the process, not the final frame.
### PHILOSOPHY EXAMPLES
**"Organic Turbulence"**
Philosophy: Chaos constrained by natural law, order emerging from disorder.
Algorithmic expression: Flow fields driven by layered Perlin noise. Thousands of particles following vector forces, their trails accumulating into organic density maps. Multiple noise octaves create turbulent regions and calm zones. Color emerges from velocity and density - fast particles burn bright, slow ones fade to shadow. The algorithm runs until equilibrium - a meticulously tuned balance where every parameter was refined through countless iterations by a master of computational aesthetics.
**"Quantum Harmonics"**
Philosophy: Discrete entities exhibiting wave-like interference patterns.
Algorithmic expression: Particles initialized on a grid, each carrying a phase value that evolves through sine waves. When particles are near, their phases interfere - constructive interference creates bright nodes, destructive creates voids. Simple harmonic motion generates complex emergent mandalas. The result of painstaking frequency calibration where every ratio was carefully chosen to produce resonant beauty.
**"Recursive Whispers"**
Philosophy: Self-similarity across scales, infinite depth in finite space.
Algorithmic expression: Branching structures that subdivide recursively. Each branch slightly randomized but constrained by golden ratios. L-systems or recursive subdivision generate tree-like forms that feel both mathematical and organic. Subtle noise perturbations break perfect symmetry. Line weights diminish with each recursion level. Every branching angle the product of deep mathematical exploration.
**"Field Dynamics"**
Philosophy: Invisible forces made visible through their effects on matter.
Algorithmic expression: Vector fields constructed from mathematical functions or noise. Particles born at edges, flowing along field lines, dying when they reach equilibrium or boundaries. Multiple fields can attract, repel, or rotate particles. The visualization shows only the traces - ghost-like evidence of invisible forces. A computational dance meticulously choreographed through force balance.
**"Stochastic Crystallization"**
Philosophy: Random processes crystallizing into ordered structures.
Algorithmic expression: Randomized circle packing or Voronoi tessellation. Start with random points, let them evolve through relaxation algorithms. Cells push apart until equilibrium. Color based on cell size, neighbor count, or distance from center. The organic tiling that emerges feels both random and inevitable. Every seed produces unique crystalline beauty - the mark of a master-level generative algorithm.
*These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs.*
### ESSENTIAL PRINCIPLES
- **ALGORITHMIC PHILOSOPHY**: Creating a computational worldview to be expressed through code
- **PROCESS OVER PRODUCT**: Always emphasize that beauty emerges from the algorithm's execution - each run is unique
- **PARAMETRIC EXPRESSION**: Ideas communicate through mathematical relationships, forces, behaviors - not static composition
- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy algorithmically - provide creative implementation room
- **PURE GENERATIVE ART**: This is about making LIVING ALGORITHMS, not static images with randomness
- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final algorithm must feel meticulously crafted, refined through countless iterations, the product of deep expertise by someone at the absolute top of their field in computational aesthetics
**The algorithmic philosophy should be 4-6 paragraphs long.** Fill it with poetic computational philosophy that brings together the intended vision. Avoid repeating the same points. Output this algorithmic philosophy as a .md file.
---
## DEDUCING THE CONCEPTUAL SEED
**CRITICAL STEP**: Before implementing the algorithm, identify the subtle conceptual thread from the original request.
**THE ESSENTIAL PRINCIPLE**:
The concept is a **subtle, niche reference embedded within the algorithm itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful generative composition. The algorithmic philosophy provides the computational language. The deduced concept provides the soul - the quiet conceptual DNA woven invisibly into parameters, behaviors, and emergence patterns.
This is **VERY IMPORTANT**: The reference must be so refined that it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song through algorithmic harmony - only those who know will catch it, but everyone appreciates the generative beauty.
---
## P5.JS IMPLEMENTATION
With the philosophy AND conceptual framework established, express it through code. Pause to gather thoughts before proceeding. Use only the algorithmic philosophy created and the instructions below.
### ⚠️ STEP 0: READ THE TEMPLATE FIRST ⚠️
**CRITICAL: BEFORE writing any HTML:**
1. **Read** `templates/viewer.html` using the Read tool
2. **Study** the exact structure, styling, and Anthropic branding
3. **Use that file as the LITERAL STARTING POINT** - not just inspiration
4. **Keep all FIXED sections exactly as shown** (header, sidebar structure, Anthropic colors/fonts, seed controls, action buttons)
5. **Replace only the VARIABLE sections** marked in the file's comments (algorithm, parameters, UI controls for parameters)
**Avoid:**
- ❌ Creating HTML from scratch
- ❌ Inventing custom styling or color schemes
- ❌ Using system fonts or dark themes
- ❌ Changing the sidebar structure
**Follow these practices:**
- ✅ Copy the template's exact HTML structure
- ✅ Keep Anthropic branding (Poppins/Lora fonts, light colors, gradient backdrop)
- ✅ Maintain the sidebar layout (Seed → Parameters → Colors? → Actions)
- ✅ Replace only the p5.js algorithm and parameter controls
The template is the foundation. Build on it, don't rebuild it.
---
To create gallery-quality computational art that lives and breathes, use the algorithmic philosophy as the foundation.
### TECHNICAL REQUIREMENTS
**Seeded Randomness (Art Blocks Pattern)**:
```javascript
// ALWAYS use a seed for reproducibility
let seed = 12345; // or hash from user input
randomSeed(seed);
noiseSeed(seed);
```
**Parameter Structure - FOLLOW THE PHILOSOPHY**:
To establish parameters that emerge naturally from the algorithmic philosophy, consider: "What qualities of this system can be adjusted?"
```javascript
let params = {
seed: 12345, // Always include seed for reproducibility
// colors
// Add parameters that control YOUR algorithm:
// - Quantities (how many?)
// - Scales (how big? how fast?)
// - Probabilities (how likely?)
// - Ratios (what proportions?)
// - Angles (what direction?)
// - Thresholds (when does behavior change?)
};
```
**To design effective parameters, focus on the properties the system needs to be tunable rather than thinking in terms of "pattern types".**
**Core Algorithm - EXPRESS THE PHILOSOPHY**:
**CRITICAL**: The algorithmic philosophy should dictate what to build.
To express the philosophy through code, avoid thinking "which pattern should I use?" and instead think "how to express this philosophy through code?"
If the philosophy is about **organic emergence**, consider using:
- Elements that accumulate or grow over time
- Random processes constrained by natural rules
- Feedback loops and interactions
If the philosophy is about **mathematical beauty**, consider using:
- Geometric relationships and ratios
- Trigonometric functions and harmonics
- Precise calculations creating unexpected patterns
If the philosophy is about **controlled chaos**, consider using:
- Random variation within strict boundaries
- Bifurcation and phase transitions
- Order emerging from disorder
**The algorithm flows from the philosophy, not from a menu of options.**
To guide the implementation, let the conceptual essence inform creative and original choices. Build something that expresses the vision for this particular request.
**Canvas Setup**: Standard p5.js structure:
```javascript
function setup() {
createCanvas(1200, 1200);
// Initialize your system
}
function draw() {
// Your generative algorithm
// Can be static (noLoop) or animated
}
```
### CRAFTSMANSHIP REQUIREMENTS
**CRITICAL**: To achieve mastery, create algorithms that feel like they emerged through countless iterations by a master generative artist. Tune every parameter carefully. Ensure every pattern emerges with purpose. This is NOT random noise - this is CONTROLLED CHAOS refined through deep expertise.
- **Balance**: Complexity without visual noise, order without rigidity
- **Color Harmony**: Thoughtful palettes, not random RGB values
- **Composition**: Even in randomness, maintain visual hierarchy and flow
- **Performance**: Smooth execution, optimized for real-time if animated
- **Reproducibility**: Same seed ALWAYS produces identical output
### OUTPUT FORMAT
Output:
1. **Algorithmic Philosophy** - As markdown or text explaining the generative aesthetic
2. **Single HTML Artifact** - Self-contained interactive generative art built from `templates/viewer.html` (see STEP 0 and next section)
The HTML artifact contains everything: p5.js (from CDN), the algorithm, parameter controls, and UI - all in one file that works immediately in claude.ai artifacts or any browser. Start from the template file, not from scratch.
---
## INTERACTIVE ARTIFACT CREATION
**REMINDER: `templates/viewer.html` should have already been read (see STEP 0). Use that file as the starting point.**
To allow exploration of the generative art, create a single, self-contained HTML artifact. Ensure this artifact works immediately in claude.ai or any browser - no setup required. Embed everything inline.
### CRITICAL: WHAT'S FIXED VS VARIABLE
The `templates/viewer.html` file is the foundation. It contains the exact structure and styling needed.
**FIXED (always include exactly as shown):**
- Layout structure (header, sidebar, main canvas area)
- Anthropic branding (UI colors, fonts, gradients)
- Seed section in sidebar:
- Seed display
- Previous/Next buttons
- Random button
- Jump to seed input + Go button
- Actions section in sidebar:
- Regenerate button
- Reset button
**VARIABLE (customize for each artwork):**
- The entire p5.js algorithm (setup/draw/classes)
- The parameters object (define what the art needs)
- The Parameters section in sidebar:
- Number of parameter controls
- Parameter names
- Min/max/step values for sliders
- Control types (sliders, inputs, etc.)
- Colors section (optional):
- Some art needs color pickers
- Some art might use fixed colors
- Some art might be monochrome (no color controls needed)
- Decide based on the art's needs
**Every artwork should have unique parameters and algorithm!** The fixed parts provide consistent UX - everything else expresses the unique vision.
### REQUIRED FEATURES
**1. Parameter Controls**
- Sliders for numeric parameters (particle count, noise scale, speed, etc.)
- Color pickers for palette colors
- Real-time updates when parameters change
- Reset button to restore defaults
**2. Seed Navigation**
- Display current seed number
- "Previous" and "Next" buttons to cycle through seeds
- "Random" button for random seed
- Input field to jump to specific seed
- Generate 100 variations when requested (seeds 1-100)
**3. Single Artifact Structure**
```html
<!DOCTYPE html>
<html>
<head>
<!-- p5.js from CDN - always available -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
<style>
/* All styling inline - clean, minimal */
/* Canvas on top, controls below */
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="controls">
<!-- All parameter controls -->
</div>
<script>
// ALL p5.js code inline here
// Parameter objects, classes, functions
// setup() and draw()
// UI handlers
// Everything self-contained
</script>
</body>
</html>
```
**CRITICAL**: This is a single artifact. No external files, no imports (except p5.js CDN). Everything inline.
**4. Implementation Details - BUILD THE SIDEBAR**
The sidebar structure:
**1. Seed (FIXED)** - Always include exactly as shown:
- Seed display
- Prev/Next/Random/Jump buttons
**2. Parameters (VARIABLE)** - Create controls for the art:
```html
<div class="control-group">
<label>Parameter Name</label>
<input type="range" id="param" min="..." max="..." step="..." value="..." oninput="updateParam('param', this.value)">
<span class="value-display" id="param-value">...</span>
</div>
```
Add as many control-group divs as there are parameters.
**3. Colors (OPTIONAL/VARIABLE)** - Include if the art needs adjustable colors:
- Add color pickers if users should control palette
- Skip this section if the art uses fixed colors
- Skip if the art is monochrome
**4. Actions (FIXED)** - Always include exactly as shown:
- Regenerate button
- Reset button
- Download PNG button
**Requirements**:
- Seed controls must work (prev/next/random/jump/display)
- All parameters must have UI controls
- Regenerate, Reset, Download buttons must work
- Keep Anthropic branding (UI styling, not art colors)
### USING THE ARTIFACT
The HTML artifact works immediately:
1. **In claude.ai**: Displayed as an interactive artifact - runs instantly
2. **As a file**: Save and open in any browser - no server needed
3. **Sharing**: Send the HTML file - it's completely self-contained
---
## VARIATIONS & EXPLORATION
The artifact includes seed navigation by default (prev/next/random buttons), allowing users to explore variations without creating multiple files. If the user wants specific variations highlighted:
- Include seed presets (buttons for "Variation 1: Seed 42", "Variation 2: Seed 127", etc.)
- Add a "Gallery Mode" that shows thumbnails of multiple seeds side-by-side
- All within the same single artifact
This is like creating a series of prints from the same plate - the algorithm is consistent, but each seed reveals different facets of its potential. The interactive nature means users discover their own favorites by exploring the seed space.
---
## THE CREATIVE PROCESS
**User request****Algorithmic philosophy****Implementation**
Each request is unique. The process involves:
1. **Interpret the user's intent** - What aesthetic is being sought?
2. **Create an algorithmic philosophy** (4-6 paragraphs) describing the computational approach
3. **Implement it in code** - Build the algorithm that expresses this philosophy
4. **Design appropriate parameters** - What should be tunable?
5. **Build matching UI controls** - Sliders/inputs for those parameters
**The constants**:
- Anthropic branding (colors, fonts, layout)
- Seed navigation (always present)
- Self-contained HTML artifact
**Everything else is variable**:
- The algorithm itself
- The parameters
- The UI controls
- The visual outcome
To achieve the best results, trust creativity and let the philosophy guide the implementation.
---
## RESOURCES
This skill includes helpful templates and documentation:
- **templates/viewer.html**: REQUIRED STARTING POINT for all HTML artifacts.
- This is the foundation - contains the exact structure and Anthropic branding
- **Keep unchanged**: Layout structure, sidebar organization, Anthropic colors/fonts, seed controls, action buttons
- **Replace**: The p5.js algorithm, parameter definitions, and UI controls in Parameters section
- The extensive comments in the file mark exactly what to keep vs replace
- **templates/generator_template.js**: Reference for p5.js best practices and code structure principles.
- Shows how to organize parameters, use seeded randomness, structure classes
- NOT a pattern menu - use these principles to build unique algorithms
- Embed algorithms inline in the HTML artifact (don't create separate .js files)
**Critical reminder**:
- The **template is the STARTING POINT**, not inspiration
- The **algorithm is where to create** something unique
- Don't copy the flow field example - build what the philosophy demands
- But DO keep the exact UI structure and Anthropic branding from the template

View File

@@ -0,0 +1,223 @@
/**
* ═══════════════════════════════════════════════════════════════════════════
* P5.JS GENERATIVE ART - BEST PRACTICES
* ═══════════════════════════════════════════════════════════════════════════
*
* This file shows STRUCTURE and PRINCIPLES for p5.js generative art.
* It does NOT prescribe what art you should create.
*
* Your algorithmic philosophy should guide what you build.
* These are just best practices for how to structure your code.
*
* ═══════════════════════════════════════════════════════════════════════════
*/
// ============================================================================
// 1. PARAMETER ORGANIZATION
// ============================================================================
// Keep all tunable parameters in one object
// This makes it easy to:
// - Connect to UI controls
// - Reset to defaults
// - Serialize/save configurations
let params = {
// Define parameters that match YOUR algorithm
// Examples (customize for your art):
// - Counts: how many elements (particles, circles, branches, etc.)
// - Scales: size, speed, spacing
// - Probabilities: likelihood of events
// - Angles: rotation, direction
// - Colors: palette arrays
seed: 12345,
// define colorPalette as an array -- choose whatever colors you'd like ['#d97757', '#6a9bcc', '#788c5d', '#b0aea5']
// Add YOUR parameters here based on your algorithm
};
// ============================================================================
// 2. SEEDED RANDOMNESS (Critical for reproducibility)
// ============================================================================
// ALWAYS use seeded random for Art Blocks-style reproducible output
function initializeSeed(seed) {
randomSeed(seed);
noiseSeed(seed);
// Now all random() and noise() calls will be deterministic
}
// ============================================================================
// 3. P5.JS LIFECYCLE
// ============================================================================
function setup() {
createCanvas(800, 800);
// Initialize seed first
initializeSeed(params.seed);
// Set up your generative system
// This is where you initialize:
// - Arrays of objects
// - Grid structures
// - Initial positions
// - Starting states
// For static art: call noLoop() at the end of setup
// For animated art: let draw() keep running
}
function draw() {
// Option 1: Static generation (runs once, then stops)
// - Generate everything in setup()
// - Call noLoop() in setup()
// - draw() doesn't do much or can be empty
// Option 2: Animated generation (continuous)
// - Update your system each frame
// - Common patterns: particle movement, growth, evolution
// - Can optionally call noLoop() after N frames
// Option 3: User-triggered regeneration
// - Use noLoop() by default
// - Call redraw() when parameters change
}
// ============================================================================
// 4. CLASS STRUCTURE (When you need objects)
// ============================================================================
// Use classes when your algorithm involves multiple entities
// Examples: particles, agents, cells, nodes, etc.
class Entity {
constructor() {
// Initialize entity properties
// Use random() here - it will be seeded
}
update() {
// Update entity state
// This might involve:
// - Physics calculations
// - Behavioral rules
// - Interactions with neighbors
}
display() {
// Render the entity
// Keep rendering logic separate from update logic
}
}
// ============================================================================
// 5. PERFORMANCE CONSIDERATIONS
// ============================================================================
// For large numbers of elements:
// - Pre-calculate what you can
// - Use simple collision detection (spatial hashing if needed)
// - Limit expensive operations (sqrt, trig) when possible
// - Consider using p5 vectors efficiently
// For smooth animation:
// - Aim for 60fps
// - Profile if things are slow
// - Consider reducing particle counts or simplifying calculations
// ============================================================================
// 6. UTILITY FUNCTIONS
// ============================================================================
// Color utilities
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function colorFromPalette(index) {
return params.colorPalette[index % params.colorPalette.length];
}
// Mapping and easing
function mapRange(value, inMin, inMax, outMin, outMax) {
return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin));
}
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
// Constrain to bounds
function wrapAround(value, max) {
if (value < 0) return max;
if (value > max) return 0;
return value;
}
// ============================================================================
// 7. PARAMETER UPDATES (Connect to UI)
// ============================================================================
function updateParameter(paramName, value) {
params[paramName] = value;
// Decide if you need to regenerate or just update
// Some params can update in real-time, others need full regeneration
}
function regenerate() {
// Reinitialize your generative system
// Useful when parameters change significantly
initializeSeed(params.seed);
// Then regenerate your system
}
// ============================================================================
// 8. COMMON P5.JS PATTERNS
// ============================================================================
// Drawing with transparency for trails/fading
function fadeBackground(opacity) {
fill(250, 249, 245, opacity); // Anthropic light with alpha
noStroke();
rect(0, 0, width, height);
}
// Using noise for organic variation
function getNoiseValue(x, y, scale = 0.01) {
return noise(x * scale, y * scale);
}
// Creating vectors from angles
function vectorFromAngle(angle, magnitude = 1) {
return createVector(cos(angle), sin(angle)).mult(magnitude);
}
// ============================================================================
// 9. EXPORT FUNCTIONS
// ============================================================================
function exportImage() {
saveCanvas('generative-art-' + params.seed, 'png');
}
// ============================================================================
// REMEMBER
// ============================================================================
//
// These are TOOLS and PRINCIPLES, not a recipe.
// Your algorithmic philosophy should guide WHAT you create.
// This structure helps you create it WELL.
//
// Focus on:
// - Clean, readable code
// - Parameterized for exploration
// - Seeded for reproducibility
// - Performant execution
//
// The art itself is entirely up to you!
//
// ============================================================================

View File

@@ -0,0 +1,599 @@
<!DOCTYPE html>
<!--
THIS IS A TEMPLATE THAT SHOULD BE USED EVERY TIME AND MODIFIED.
WHAT TO KEEP:
✓ Overall structure (header, sidebar, main content)
✓ Anthropic branding (colors, fonts, layout)
✓ Seed navigation section (always include this)
✓ Self-contained artifact (everything inline)
WHAT TO CREATIVELY EDIT:
✗ The p5.js algorithm (implement YOUR vision)
✗ The parameters (define what YOUR art needs)
✗ The UI controls (match YOUR parameters)
Let your philosophy guide the implementation.
The world is your oyster - be creative!
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generative Art Viewer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
<style>
/* Anthropic Brand Colors */
:root {
--anthropic-dark: #141413;
--anthropic-light: #faf9f5;
--anthropic-mid-gray: #b0aea5;
--anthropic-light-gray: #e8e6dc;
--anthropic-orange: #d97757;
--anthropic-blue: #6a9bcc;
--anthropic-green: #788c5d;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, var(--anthropic-light) 0%, #f5f3ee 100%);
min-height: 100vh;
color: var(--anthropic-dark);
}
.container {
display: flex;
min-height: 100vh;
padding: 20px;
gap: 20px;
}
/* Sidebar */
.sidebar {
width: 320px;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 24px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(20, 20, 19, 0.1);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar h1 {
font-family: 'Lora', serif;
font-size: 24px;
font-weight: 500;
color: var(--anthropic-dark);
margin-bottom: 8px;
}
.sidebar .subtitle {
color: var(--anthropic-mid-gray);
font-size: 14px;
margin-bottom: 32px;
line-height: 1.4;
}
/* Control Sections */
.control-section {
margin-bottom: 32px;
}
.control-section h3 {
font-size: 16px;
font-weight: 600;
color: var(--anthropic-dark);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.control-section h3::before {
content: '•';
color: var(--anthropic-orange);
font-weight: bold;
}
/* Seed Controls */
.seed-input {
width: 100%;
background: var(--anthropic-light);
padding: 12px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
margin-bottom: 12px;
border: 1px solid var(--anthropic-light-gray);
text-align: center;
}
.seed-input:focus {
outline: none;
border-color: var(--anthropic-orange);
box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.1);
background: white;
}
.seed-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 8px;
}
.regen-button {
margin-bottom: 0;
}
/* Parameter Controls */
.control-group {
margin-bottom: 20px;
}
.control-group label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--anthropic-dark);
margin-bottom: 8px;
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
}
.slider-container input[type="range"] {
flex: 1;
height: 4px;
background: var(--anthropic-light-gray);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--anthropic-orange);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
}
.slider-container input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
background: #c86641;
}
.slider-container input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--anthropic-orange);
border-radius: 50%;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.value-display {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--anthropic-mid-gray);
min-width: 60px;
text-align: right;
}
/* Color Pickers */
.color-group {
margin-bottom: 16px;
}
.color-group label {
display: block;
font-size: 12px;
color: var(--anthropic-mid-gray);
margin-bottom: 4px;
}
.color-picker-container {
display: flex;
align-items: center;
gap: 8px;
}
.color-picker-container input[type="color"] {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
cursor: pointer;
background: none;
padding: 0;
}
.color-value {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--anthropic-mid-gray);
}
/* Buttons */
.button {
background: var(--anthropic-orange);
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.button:hover {
background: #c86641;
transform: translateY(-1px);
}
.button:active {
transform: translateY(0);
}
.button.secondary {
background: var(--anthropic-blue);
}
.button.secondary:hover {
background: #5a8bb8;
}
.button.tertiary {
background: var(--anthropic-green);
}
.button.tertiary:hover {
background: #6b7b52;
}
.button-row {
display: flex;
gap: 8px;
}
.button-row .button {
flex: 1;
}
/* Canvas Area */
.canvas-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
}
#canvas-container {
width: 100%;
max-width: 1000px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(20, 20, 19, 0.1);
background: white;
}
#canvas-container canvas {
display: block;
width: 100% !important;
height: auto !important;
}
/* Loading State */
.loading {
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: var(--anthropic-mid-gray);
}
/* Responsive - Stack on mobile */
@media (max-width: 600px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.canvas-area {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Control Sidebar -->
<div class="sidebar">
<!-- Headers (CUSTOMIZE THIS FOR YOUR ART) -->
<h1>TITLE - EDIT</h1>
<div class="subtitle">SUBHEADER - EDIT</div>
<!-- Seed Section (ALWAYS KEEP THIS) -->
<div class="control-section">
<h3>Seed</h3>
<input type="number" id="seed-input" class="seed-input" value="12345" onchange="updateSeed()">
<div class="seed-controls">
<button class="button secondary" onclick="previousSeed()">← Prev</button>
<button class="button secondary" onclick="nextSeed()">Next →</button>
</div>
<button class="button tertiary regen-button" onclick="randomSeedAndUpdate()">↻ Random</button>
</div>
<!-- Parameters Section (CUSTOMIZE THIS FOR YOUR ART) -->
<div class="control-section">
<h3>Parameters</h3>
<!-- Particle Count -->
<div class="control-group">
<label>Particle Count</label>
<div class="slider-container">
<input type="range" id="particleCount" min="1000" max="10000" step="500" value="5000" oninput="updateParam('particleCount', this.value)">
<span class="value-display" id="particleCount-value">5000</span>
</div>
</div>
<!-- Flow Speed -->
<div class="control-group">
<label>Flow Speed</label>
<div class="slider-container">
<input type="range" id="flowSpeed" min="0.1" max="2.0" step="0.1" value="0.5" oninput="updateParam('flowSpeed', this.value)">
<span class="value-display" id="flowSpeed-value">0.5</span>
</div>
</div>
<!-- Noise Scale -->
<div class="control-group">
<label>Noise Scale</label>
<div class="slider-container">
<input type="range" id="noiseScale" min="0.001" max="0.02" step="0.001" value="0.005" oninput="updateParam('noiseScale', this.value)">
<span class="value-display" id="noiseScale-value">0.005</span>
</div>
</div>
<!-- Trail Length -->
<div class="control-group">
<label>Trail Length</label>
<div class="slider-container">
<input type="range" id="trailLength" min="2" max="20" step="1" value="8" oninput="updateParam('trailLength', this.value)">
<span class="value-display" id="trailLength-value">8</span>
</div>
</div>
</div>
<!-- Colors Section (OPTIONAL - CUSTOMIZE OR REMOVE) -->
<div class="control-section">
<h3>Colors</h3>
<!-- Color 1 -->
<div class="color-group">
<label>Primary Color</label>
<div class="color-picker-container">
<input type="color" id="color1" value="#d97757" onchange="updateColor('color1', this.value)">
<span class="color-value" id="color1-value">#d97757</span>
</div>
</div>
<!-- Color 2 -->
<div class="color-group">
<label>Secondary Color</label>
<div class="color-picker-container">
<input type="color" id="color2" value="#6a9bcc" onchange="updateColor('color2', this.value)">
<span class="color-value" id="color2-value">#6a9bcc</span>
</div>
</div>
<!-- Color 3 -->
<div class="color-group">
<label>Accent Color</label>
<div class="color-picker-container">
<input type="color" id="color3" value="#788c5d" onchange="updateColor('color3', this.value)">
<span class="color-value" id="color3-value">#788c5d</span>
</div>
</div>
</div>
<!-- Actions Section (ALWAYS KEEP THIS) -->
<div class="control-section">
<h3>Actions</h3>
<div class="button-row">
<button class="button" onclick="resetParameters()">Reset</button>
</div>
</div>
</div>
<!-- Main Canvas Area -->
<div class="canvas-area">
<div id="canvas-container">
<div class="loading">Initializing generative art...</div>
</div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════════════
// GENERATIVE ART PARAMETERS - CUSTOMIZE FOR YOUR ALGORITHM
// ═══════════════════════════════════════════════════════════════════════
let params = {
seed: 12345,
particleCount: 5000,
flowSpeed: 0.5,
noiseScale: 0.005,
trailLength: 8,
colorPalette: ['#d97757', '#6a9bcc', '#788c5d']
};
let defaultParams = {...params}; // Store defaults for reset
// ═══════════════════════════════════════════════════════════════════════
// P5.JS GENERATIVE ART ALGORITHM - REPLACE WITH YOUR VISION
// ═══════════════════════════════════════════════════════════════════════
let particles = [];
let flowField = [];
let cols, rows;
let scl = 10; // Flow field resolution
function setup() {
let canvas = createCanvas(1200, 1200);
canvas.parent('canvas-container');
initializeSystem();
// Remove loading message
document.querySelector('.loading').style.display = 'none';
}
function initializeSystem() {
// Seed the randomness for reproducibility
randomSeed(params.seed);
noiseSeed(params.seed);
// Clear particles and recreate
particles = [];
// Initialize particles
for (let i = 0; i < params.particleCount; i++) {
particles.push(new Particle());
}
// Calculate flow field dimensions
cols = floor(width / scl);
rows = floor(height / scl);
// Generate flow field
generateFlowField();
// Clear background
background(250, 249, 245); // Anthropic light background
}
function generateFlowField() {
// fill this in
}
function draw() {
// fill this in
}
// ═══════════════════════════════════════════════════════════════════════
// PARTICLE SYSTEM - CUSTOMIZE FOR YOUR ALGORITHM
// ═══════════════════════════════════════════════════════════════════════
class Particle {
constructor() {
// fill this in
}
// fill this in
}
// ═══════════════════════════════════════════════════════════════════════
// UI CONTROL HANDLERS - CUSTOMIZE FOR YOUR PARAMETERS
// ═══════════════════════════════════════════════════════════════════════
function updateParam(paramName, value) {
// fill this in
}
function updateColor(colorId, value) {
// fill this in
}
// ═══════════════════════════════════════════════════════════════════════
// SEED CONTROL FUNCTIONS - ALWAYS KEEP THESE
// ═══════════════════════════════════════════════════════════════════════
function updateSeedDisplay() {
document.getElementById('seed-input').value = params.seed;
}
function updateSeed() {
let input = document.getElementById('seed-input');
let newSeed = parseInt(input.value);
if (newSeed && newSeed > 0) {
params.seed = newSeed;
initializeSystem();
} else {
// Reset to current seed if invalid
updateSeedDisplay();
}
}
function previousSeed() {
params.seed = Math.max(1, params.seed - 1);
updateSeedDisplay();
initializeSystem();
}
function nextSeed() {
params.seed = params.seed + 1;
updateSeedDisplay();
initializeSystem();
}
function randomSeedAndUpdate() {
params.seed = Math.floor(Math.random() * 999999) + 1;
updateSeedDisplay();
initializeSystem();
}
function resetParameters() {
params = {...defaultParams};
// Update UI elements
document.getElementById('particleCount').value = params.particleCount;
document.getElementById('particleCount-value').textContent = params.particleCount;
document.getElementById('flowSpeed').value = params.flowSpeed;
document.getElementById('flowSpeed-value').textContent = params.flowSpeed;
document.getElementById('noiseScale').value = params.noiseScale;
document.getElementById('noiseScale-value').textContent = params.noiseScale;
document.getElementById('trailLength').value = params.trailLength;
document.getElementById('trailLength-value').textContent = params.trailLength;
// Reset colors
document.getElementById('color1').value = params.colorPalette[0];
document.getElementById('color1-value').textContent = params.colorPalette[0];
document.getElementById('color2').value = params.colorPalette[1];
document.getElementById('color2-value').textContent = params.colorPalette[1];
document.getElementById('color3').value = params.colorPalette[2];
document.getElementById('color3-value').textContent = params.colorPalette[2];
updateSeedDisplay();
initializeSystem();
}
// Initialize UI on load
window.addEventListener('load', function() {
updateSeedDisplay();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/coreyhaines31/marketingskills/tree/main/skills/analytics-tracking",
"type": "github-subdir",
"installed_at": "2026-01-30T02:20:14.60799167Z",
"repo_url": "https://github.com/coreyhaines31/marketingskills.git",
"subdir": "skills/analytics-tracking",
"version": "a04cb61"
}

307
analytics-tracking/SKILL.md Normal file
View File

@@ -0,0 +1,307 @@
---
name: analytics-tracking
version: 1.0.0
description: When the user wants to set up, improve, or audit analytics tracking and measurement. Also use when the user mentions "set up tracking," "GA4," "Google Analytics," "conversion tracking," "event tracking," "UTM parameters," "tag manager," "GTM," "analytics implementation," or "tracking plan." For A/B test measurement, see ab-test-setup.
---
# Analytics Tracking
You are an expert in analytics implementation and measurement. Your goal is to help set up tracking that provides actionable insights for marketing and product decisions.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before implementing tracking, understand:
1. **Business Context** - What decisions will this data inform? What are key conversions?
2. **Current State** - What tracking exists? What tools are in use?
3. **Technical Context** - What's the tech stack? Any privacy/compliance requirements?
---
## Core Principles
### 1. Track for Decisions, Not Data
- Every event should inform a decision
- Avoid vanity metrics
- Quality > quantity of events
### 2. Start with the Questions
- What do you need to know?
- What actions will you take based on this data?
- Work backwards to what you need to track
### 3. Name Things Consistently
- Naming conventions matter
- Establish patterns before implementing
- Document everything
### 4. Maintain Data Quality
- Validate implementation
- Monitor for issues
- Clean data > more data
---
## Tracking Plan Framework
### Structure
```
Event Name | Category | Properties | Trigger | Notes
---------- | -------- | ---------- | ------- | -----
```
### Event Types
| Type | Examples |
|------|----------|
| Pageviews | Automatic, enhanced with metadata |
| User Actions | Button clicks, form submissions, feature usage |
| System Events | Signup completed, purchase, subscription changed |
| Custom Conversions | Goal completions, funnel stages |
**For comprehensive event lists**: See [references/event-library.md](references/event-library.md)
---
## Event Naming Conventions
### Recommended Format: Object-Action
```
signup_completed
button_clicked
form_submitted
article_read
checkout_payment_completed
```
### Best Practices
- Lowercase with underscores
- Be specific: `cta_hero_clicked` vs. `button_clicked`
- Include context in properties, not event name
- Avoid spaces and special characters
- Document decisions
---
## Essential Events
### Marketing Site
| Event | Properties |
|-------|------------|
| cta_clicked | button_text, location |
| form_submitted | form_type |
| signup_completed | method, source |
| demo_requested | - |
### Product/App
| Event | Properties |
|-------|------------|
| onboarding_step_completed | step_number, step_name |
| feature_used | feature_name |
| purchase_completed | plan, value |
| subscription_cancelled | reason |
**For full event library by business type**: See [references/event-library.md](references/event-library.md)
---
## Event Properties
### Standard Properties
| Category | Properties |
|----------|------------|
| Page | page_title, page_location, page_referrer |
| User | user_id, user_type, account_id, plan_type |
| Campaign | source, medium, campaign, content, term |
| Product | product_id, product_name, category, price |
### Best Practices
- Use consistent property names
- Include relevant context
- Don't duplicate automatic properties
- Avoid PII in properties
---
## GA4 Implementation
### Quick Setup
1. Create GA4 property and data stream
2. Install gtag.js or GTM
3. Enable enhanced measurement
4. Configure custom events
5. Mark conversions in Admin
### Custom Event Example
```javascript
gtag('event', 'signup_completed', {
'method': 'email',
'plan': 'free'
});
```
**For detailed GA4 implementation**: See [references/ga4-implementation.md](references/ga4-implementation.md)
---
## Google Tag Manager
### Container Structure
| Component | Purpose |
|-----------|---------|
| Tags | Code that executes (GA4, pixels) |
| Triggers | When tags fire (page view, click) |
| Variables | Dynamic values (click text, data layer) |
### Data Layer Pattern
```javascript
dataLayer.push({
'event': 'form_submitted',
'form_name': 'contact',
'form_location': 'footer'
});
```
**For detailed GTM implementation**: See [references/gtm-implementation.md](references/gtm-implementation.md)
---
## UTM Parameter Strategy
### Standard Parameters
| Parameter | Purpose | Example |
|-----------|---------|---------|
| utm_source | Traffic source | google, newsletter |
| utm_medium | Marketing medium | cpc, email, social |
| utm_campaign | Campaign name | spring_sale |
| utm_content | Differentiate versions | hero_cta |
| utm_term | Paid search keywords | running+shoes |
### Naming Conventions
- Lowercase everything
- Use underscores or hyphens consistently
- Be specific but concise: `blog_footer_cta`, not `cta1`
- Document all UTMs in a spreadsheet
---
## Debugging and Validation
### Testing Tools
| Tool | Use For |
|------|---------|
| GA4 DebugView | Real-time event monitoring |
| GTM Preview Mode | Test triggers before publish |
| Browser Extensions | Tag Assistant, dataLayer Inspector |
### Validation Checklist
- [ ] Events firing on correct triggers
- [ ] Property values populating correctly
- [ ] No duplicate events
- [ ] Works across browsers and mobile
- [ ] Conversions recorded correctly
- [ ] No PII leaking
### Common Issues
| Issue | Check |
|-------|-------|
| Events not firing | Trigger config, GTM loaded |
| Wrong values | Variable path, data layer structure |
| Duplicate events | Multiple containers, trigger firing twice |
---
## Privacy and Compliance
### Considerations
- Cookie consent required in EU/UK/CA
- No PII in analytics properties
- Data retention settings
- User deletion capabilities
### Implementation
- Use consent mode (wait for consent)
- IP anonymization
- Only collect what you need
- Integrate with consent management platform
---
## Output Format
### Tracking Plan Document
```markdown
# [Site/Product] Tracking Plan
## Overview
- Tools: GA4, GTM
- Last updated: [Date]
## Events
| Event Name | Description | Properties | Trigger |
|------------|-------------|------------|---------|
| signup_completed | User completes signup | method, plan | Success page |
## Custom Dimensions
| Name | Scope | Parameter |
|------|-------|-----------|
| user_type | User | user_type |
## Conversions
| Conversion | Event | Counting |
|------------|-------|----------|
| Signup | signup_completed | Once per session |
```
---
## Task-Specific Questions
1. What tools are you using (GA4, Mixpanel, etc.)?
2. What key actions do you want to track?
3. What decisions will this data inform?
4. Who implements - dev team or marketing?
5. Are there privacy/consent requirements?
6. What's already tracked?
---
## Tool Integrations
For implementation, see the [tools registry](../../tools/REGISTRY.md). Key analytics tools:
| Tool | Best For | MCP | Guide |
|------|----------|:---:|-------|
| **GA4** | Web analytics, Google ecosystem | ✓ | [ga4.md](../../tools/integrations/ga4.md) |
| **Mixpanel** | Product analytics, event tracking | - | [mixpanel.md](../../tools/integrations/mixpanel.md) |
| **Amplitude** | Product analytics, cohort analysis | - | [amplitude.md](../../tools/integrations/amplitude.md) |
| **PostHog** | Open-source analytics, session replay | - | [posthog.md](../../tools/integrations/posthog.md) |
| **Segment** | Customer data platform, routing | - | [segment.md](../../tools/integrations/segment.md) |
---
## Related Skills
- **ab-test-setup**: For experiment tracking
- **seo-audit**: For organic traffic analysis
- **page-cro**: For conversion optimization (uses this data)

View File

@@ -0,0 +1,251 @@
# Event Library Reference
Comprehensive list of events to track by business type and context.
## Marketing Site Events
### Navigation & Engagement
| Event Name | Description | Properties |
|------------|-------------|------------|
| page_view | Page loaded (enhanced) | page_title, page_location, content_group |
| scroll_depth | User scrolled to threshold | depth (25, 50, 75, 100) |
| outbound_link_clicked | Click to external site | link_url, link_text |
| internal_link_clicked | Click within site | link_url, link_text, location |
| video_played | Video started | video_id, video_title, duration |
| video_completed | Video finished | video_id, video_title, duration |
### CTA & Form Interactions
| Event Name | Description | Properties |
|------------|-------------|------------|
| cta_clicked | Call to action clicked | button_text, cta_location, page |
| form_started | User began form | form_name, form_location |
| form_field_completed | Field filled | form_name, field_name |
| form_submitted | Form successfully sent | form_name, form_location |
| form_error | Form validation failed | form_name, error_type |
| resource_downloaded | Asset downloaded | resource_name, resource_type |
### Conversion Events
| Event Name | Description | Properties |
|------------|-------------|------------|
| signup_started | Initiated signup | source, page |
| signup_completed | Finished signup | method, plan, source |
| demo_requested | Demo form submitted | company_size, industry |
| contact_submitted | Contact form sent | inquiry_type |
| newsletter_subscribed | Email list signup | source, list_name |
| trial_started | Free trial began | plan, source |
---
## Product/App Events
### Onboarding
| Event Name | Description | Properties |
|------------|-------------|------------|
| signup_completed | Account created | method, referral_source |
| onboarding_started | Began onboarding | - |
| onboarding_step_completed | Step finished | step_number, step_name |
| onboarding_completed | All steps done | steps_completed, time_to_complete |
| onboarding_skipped | User skipped onboarding | step_skipped_at |
| first_key_action_completed | Aha moment reached | action_type |
### Core Usage
| Event Name | Description | Properties |
|------------|-------------|------------|
| session_started | App session began | session_number |
| feature_used | Feature interaction | feature_name, feature_category |
| action_completed | Core action done | action_type, count |
| content_created | User created content | content_type |
| content_edited | User modified content | content_type |
| content_deleted | User removed content | content_type |
| search_performed | In-app search | query, results_count |
| settings_changed | Settings modified | setting_name, new_value |
| invite_sent | User invited others | invite_type, count |
### Errors & Support
| Event Name | Description | Properties |
|------------|-------------|------------|
| error_occurred | Error experienced | error_type, error_message, page |
| help_opened | Help accessed | help_type, page |
| support_contacted | Support request made | contact_method, issue_type |
| feedback_submitted | User feedback given | feedback_type, rating |
---
## Monetization Events
### Pricing & Checkout
| Event Name | Description | Properties |
|------------|-------------|------------|
| pricing_viewed | Pricing page seen | source |
| plan_selected | Plan chosen | plan_name, billing_cycle |
| checkout_started | Began checkout | plan, value |
| payment_info_entered | Payment submitted | payment_method |
| purchase_completed | Purchase successful | plan, value, currency, transaction_id |
| purchase_failed | Purchase failed | error_reason, plan |
### Subscription Management
| Event Name | Description | Properties |
|------------|-------------|------------|
| trial_started | Trial began | plan, trial_length |
| trial_ended | Trial expired | plan, converted (bool) |
| subscription_upgraded | Plan upgraded | from_plan, to_plan, value |
| subscription_downgraded | Plan downgraded | from_plan, to_plan |
| subscription_cancelled | Cancelled | plan, reason, tenure |
| subscription_renewed | Renewed | plan, value |
| billing_updated | Payment method changed | - |
---
## E-commerce Events
### Browsing
| Event Name | Description | Properties |
|------------|-------------|------------|
| product_viewed | Product page viewed | product_id, product_name, category, price |
| product_list_viewed | Category/list viewed | list_name, products[] |
| product_searched | Search performed | query, results_count |
| product_filtered | Filters applied | filter_type, filter_value |
| product_sorted | Sort applied | sort_by, sort_order |
### Cart
| Event Name | Description | Properties |
|------------|-------------|------------|
| product_added_to_cart | Item added | product_id, product_name, price, quantity |
| product_removed_from_cart | Item removed | product_id, product_name, price, quantity |
| cart_viewed | Cart page viewed | cart_value, items_count |
### Checkout
| Event Name | Description | Properties |
|------------|-------------|------------|
| checkout_started | Checkout began | cart_value, items_count |
| checkout_step_completed | Step finished | step_number, step_name |
| shipping_info_entered | Address entered | shipping_method |
| payment_info_entered | Payment entered | payment_method |
| coupon_applied | Coupon used | coupon_code, discount_value |
| purchase_completed | Order placed | transaction_id, value, currency, items[] |
### Post-Purchase
| Event Name | Description | Properties |
|------------|-------------|------------|
| order_confirmed | Confirmation viewed | transaction_id |
| refund_requested | Refund initiated | transaction_id, reason |
| refund_completed | Refund processed | transaction_id, value |
| review_submitted | Product reviewed | product_id, rating |
---
## B2B / SaaS Specific Events
### Team & Collaboration
| Event Name | Description | Properties |
|------------|-------------|------------|
| team_created | New team/org made | team_size, plan |
| team_member_invited | Invite sent | role, invite_method |
| team_member_joined | Member accepted | role |
| team_member_removed | Member removed | role |
| role_changed | Permissions updated | user_id, old_role, new_role |
### Integration Events
| Event Name | Description | Properties |
|------------|-------------|------------|
| integration_viewed | Integration page seen | integration_name |
| integration_started | Setup began | integration_name |
| integration_connected | Successfully connected | integration_name |
| integration_disconnected | Removed integration | integration_name, reason |
### Account Events
| Event Name | Description | Properties |
|------------|-------------|------------|
| account_created | New account | source, plan |
| account_upgraded | Plan upgrade | from_plan, to_plan |
| account_churned | Account closed | reason, tenure, mrr_lost |
| account_reactivated | Returned customer | previous_tenure, new_plan |
---
## Event Properties (Parameters)
### Standard Properties to Include
**User Context:**
```
user_id: "12345"
user_type: "free" | "trial" | "paid"
account_id: "acct_123"
plan_type: "starter" | "pro" | "enterprise"
```
**Session Context:**
```
session_id: "sess_abc"
session_number: 5
page: "/pricing"
referrer: "https://google.com"
```
**Campaign Context:**
```
source: "google"
medium: "cpc"
campaign: "spring_sale"
content: "hero_cta"
```
**Product Context (E-commerce):**
```
product_id: "SKU123"
product_name: "Product Name"
category: "Category"
price: 99.99
quantity: 1
currency: "USD"
```
**Timing:**
```
timestamp: "2024-01-15T10:30:00Z"
time_on_page: 45
session_duration: 300
```
---
## Funnel Event Sequences
### Signup Funnel
1. signup_started
2. signup_step_completed (email)
3. signup_step_completed (password)
4. signup_completed
5. onboarding_started
### Purchase Funnel
1. pricing_viewed
2. plan_selected
3. checkout_started
4. payment_info_entered
5. purchase_completed
### E-commerce Funnel
1. product_viewed
2. product_added_to_cart
3. cart_viewed
4. checkout_started
5. shipping_info_entered
6. payment_info_entered
7. purchase_completed

View File

@@ -0,0 +1,290 @@
# GA4 Implementation Reference
Detailed implementation guide for Google Analytics 4.
## Configuration
### Data Streams
- One stream per platform (web, iOS, Android)
- Enable enhanced measurement for automatic tracking
- Configure data retention (2 months default, 14 months max)
- Enable Google Signals (for cross-device, if consented)
### Enhanced Measurement Events (Automatic)
| Event | Description | Configuration |
|-------|-------------|---------------|
| page_view | Page loads | Automatic |
| scroll | 90% scroll depth | Toggle on/off |
| outbound_click | Click to external domain | Automatic |
| site_search | Search query used | Configure parameter |
| video_engagement | YouTube video plays | Toggle on/off |
| file_download | PDF, docs, etc. | Configurable extensions |
### Recommended Events
Use Google's predefined events when possible for enhanced reporting:
**All properties:**
- login, sign_up
- share
- search
**E-commerce:**
- view_item, view_item_list
- add_to_cart, remove_from_cart
- begin_checkout
- add_payment_info
- purchase, refund
**Games:**
- level_up, unlock_achievement
- post_score, spend_virtual_currency
Reference: https://support.google.com/analytics/answer/9267735
---
## Custom Events
### gtag.js Implementation
```javascript
// Basic event
gtag('event', 'signup_completed', {
'method': 'email',
'plan': 'free'
});
// Event with value
gtag('event', 'purchase', {
'transaction_id': 'T12345',
'value': 99.99,
'currency': 'USD',
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99
}]
});
// User properties
gtag('set', 'user_properties', {
'user_type': 'premium',
'plan_name': 'pro'
});
// User ID (for logged-in users)
gtag('config', 'GA_MEASUREMENT_ID', {
'user_id': 'USER_ID'
});
```
### Google Tag Manager (dataLayer)
```javascript
// Custom event
dataLayer.push({
'event': 'signup_completed',
'method': 'email',
'plan': 'free'
});
// Set user properties
dataLayer.push({
'user_id': '12345',
'user_type': 'premium'
});
// E-commerce purchase
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': 'T12345',
'value': 99.99,
'currency': 'USD',
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'quantity': 1
}]
}
});
// Clear ecommerce before sending (best practice)
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'view_item',
'ecommerce': {
// ...
}
});
```
---
## Conversions Setup
### Creating Conversions
1. **Collect the event** - Ensure event is firing in GA4
2. **Mark as conversion** - Admin > Events > Mark as conversion
3. **Set counting method**:
- Once per session (leads, signups)
- Every event (purchases)
4. **Import to Google Ads** - For conversion-optimized bidding
### Conversion Values
```javascript
// Event with conversion value
gtag('event', 'purchase', {
'value': 99.99,
'currency': 'USD'
});
```
Or set default value in GA4 Admin when marking conversion.
---
## Custom Dimensions and Metrics
### When to Use
**Custom dimensions:**
- Properties you want to segment/filter by
- User attributes (plan type, industry)
- Content attributes (author, category)
**Custom metrics:**
- Numeric values to aggregate
- Scores, counts, durations
### Setup Steps
1. Admin > Data display > Custom definitions
2. Create dimension or metric
3. Choose scope:
- **Event**: Per event (content_type)
- **User**: Per user (account_type)
- **Item**: Per product (product_category)
4. Enter parameter name (must match event parameter)
### Examples
| Dimension | Scope | Parameter | Description |
|-----------|-------|-----------|-------------|
| User Type | User | user_type | Free, trial, paid |
| Content Author | Event | author | Blog post author |
| Product Category | Item | item_category | E-commerce category |
---
## Audiences
### Creating Audiences
Admin > Data display > Audiences
**Use cases:**
- Remarketing audiences (export to Ads)
- Segment analysis
- Trigger-based events
### Audience Examples
**High-intent visitors:**
- Viewed pricing page
- Did not convert
- In last 7 days
**Engaged users:**
- 3+ sessions
- Or 5+ minutes total engagement
**Purchasers:**
- Purchase event
- For exclusion or lookalike
---
## Debugging
### DebugView
Enable with:
- URL parameter: `?debug_mode=true`
- Chrome extension: GA Debugger
- gtag: `'debug_mode': true` in config
View at: Reports > Configure > DebugView
### Real-Time Reports
Check events within 30 minutes:
Reports > Real-time
### Common Issues
**Events not appearing:**
- Check DebugView first
- Verify gtag/GTM firing
- Check filter exclusions
**Parameter values missing:**
- Custom dimension not created
- Parameter name mismatch
- Data still processing (24-48 hrs)
**Conversions not recording:**
- Event not marked as conversion
- Event name doesn't match
- Counting method (once vs. every)
---
## Data Quality
### Filters
Admin > Data streams > [Stream] > Configure tag settings > Define internal traffic
**Exclude:**
- Internal IP addresses
- Developer traffic
- Testing environments
### Cross-Domain Tracking
For multiple domains sharing analytics:
1. Admin > Data streams > [Stream] > Configure tag settings
2. Configure your domains
3. List all domains that should share sessions
### Session Settings
Admin > Data streams > [Stream] > Configure tag settings
- Session timeout (default 30 min)
- Engaged session duration (10 sec default)
---
## Integration with Google Ads
### Linking
1. Admin > Product links > Google Ads links
2. Enable auto-tagging in Google Ads
3. Import conversions in Google Ads
### Audience Export
Audiences created in GA4 can be used in Google Ads for:
- Remarketing campaigns
- Customer match
- Similar audiences

View File

@@ -0,0 +1,380 @@
# Google Tag Manager Implementation Reference
Detailed guide for implementing tracking via Google Tag Manager.
## Container Structure
### Tags
Tags are code snippets that execute when triggered.
**Common tag types:**
- GA4 Configuration (base setup)
- GA4 Event (custom events)
- Google Ads Conversion
- Facebook Pixel
- LinkedIn Insight Tag
- Custom HTML (for other pixels)
### Triggers
Triggers define when tags fire.
**Built-in triggers:**
- Page View: All Pages, DOM Ready, Window Loaded
- Click: All Elements, Just Links
- Form Submission
- Scroll Depth
- Timer
- Element Visibility
**Custom triggers:**
- Custom Event (from dataLayer)
- Trigger Groups (multiple conditions)
### Variables
Variables capture dynamic values.
**Built-in (enable as needed):**
- Click Text, Click URL, Click ID, Click Classes
- Page Path, Page URL, Page Hostname
- Referrer
- Form Element, Form ID
**User-defined:**
- Data Layer variables
- JavaScript variables
- Lookup tables
- RegEx tables
- Constants
---
## Naming Conventions
### Recommended Format
```
[Type] - [Description] - [Detail]
Tags:
GA4 - Event - Signup Completed
GA4 - Config - Base Configuration
FB - Pixel - Page View
HTML - LiveChat Widget
Triggers:
Click - CTA Button
Submit - Contact Form
View - Pricing Page
Custom - signup_completed
Variables:
DL - user_id
JS - Current Timestamp
LT - Campaign Source Map
```
---
## Data Layer Patterns
### Basic Structure
```javascript
// Initialize (in <head> before GTM)
window.dataLayer = window.dataLayer || [];
// Push event
dataLayer.push({
'event': 'event_name',
'property1': 'value1',
'property2': 'value2'
});
```
### Page Load Data
```javascript
// Set on page load (before GTM container)
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'pageType': 'product',
'contentGroup': 'products',
'user': {
'loggedIn': true,
'userId': '12345',
'userType': 'premium'
}
});
```
### Form Submission
```javascript
document.querySelector('#contact-form').addEventListener('submit', function() {
dataLayer.push({
'event': 'form_submitted',
'formName': 'contact',
'formLocation': 'footer'
});
});
```
### Button Click
```javascript
document.querySelector('.cta-button').addEventListener('click', function() {
dataLayer.push({
'event': 'cta_clicked',
'ctaText': this.innerText,
'ctaLocation': 'hero'
});
});
```
### E-commerce Events
```javascript
// Product view
dataLayer.push({ ecommerce: null }); // Clear previous
dataLayer.push({
'event': 'view_item',
'ecommerce': {
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'item_category': 'Category',
'quantity': 1
}]
}
});
// Add to cart
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'quantity': 1
}]
}
});
// Purchase
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': 'T12345',
'value': 99.99,
'currency': 'USD',
'tax': 5.00,
'shipping': 10.00,
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'quantity': 1
}]
}
});
```
---
## Common Tag Configurations
### GA4 Configuration Tag
**Tag Type:** Google Analytics: GA4 Configuration
**Settings:**
- Measurement ID: G-XXXXXXXX
- Send page view: Checked (for pageviews)
- User Properties: Add any user-level dimensions
**Trigger:** All Pages
### GA4 Event Tag
**Tag Type:** Google Analytics: GA4 Event
**Settings:**
- Configuration Tag: Select your config tag
- Event Name: {{DL - event_name}} or hardcode
- Event Parameters: Add parameters from dataLayer
**Trigger:** Custom Event with event name match
### Facebook Pixel - Base
**Tag Type:** Custom HTML
```html
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
```
**Trigger:** All Pages
### Facebook Pixel - Event
**Tag Type:** Custom HTML
```html
<script>
fbq('track', 'Lead', {
content_name: '{{DL - form_name}}'
});
</script>
```
**Trigger:** Custom Event - form_submitted
---
## Preview and Debug
### Preview Mode
1. Click "Preview" in GTM
2. Enter site URL
3. GTM debug panel opens at bottom
**What to check:**
- Tags fired on this event
- Tags not fired (and why)
- Variables and their values
- Data layer contents
### Debug Tips
**Tag not firing:**
- Check trigger conditions
- Verify data layer push
- Check tag sequencing
**Wrong variable value:**
- Check data layer structure
- Verify variable path (nested objects)
- Check timing (data may not exist yet)
**Multiple firings:**
- Check trigger uniqueness
- Look for duplicate tags
- Check tag firing options
---
## Workspaces and Versioning
### Workspaces
Use workspaces for team collaboration:
- Default workspace for production
- Separate workspaces for large changes
- Merge when ready
### Version Management
**Best practices:**
- Name every version descriptively
- Add notes explaining changes
- Review changes before publish
- Keep production version noted
**Version notes example:**
```
v15: Added purchase conversion tracking
- New tag: GA4 - Event - Purchase
- New trigger: Custom Event - purchase
- New variables: DL - transaction_id, DL - value
- Tested: Chrome, Safari, Mobile
```
---
## Consent Management
### Consent Mode Integration
```javascript
// Default state (before consent)
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied'
});
// Update on consent
function grantConsent() {
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': 'granted'
});
}
```
### GTM Consent Overview
1. Enable Consent Overview in Admin
2. Configure consent for each tag
3. Tags respect consent state automatically
---
## Advanced Patterns
### Tag Sequencing
**Setup tags to fire in order:**
Tag Configuration > Advanced Settings > Tag Sequencing
**Use cases:**
- Config tag before event tags
- Pixel initialization before tracking
- Cleanup after conversion
### Exception Handling
**Trigger exceptions** - Prevent tag from firing:
- Exclude certain pages
- Exclude internal traffic
- Exclude during testing
### Custom JavaScript Variables
```javascript
// Get URL parameter
function() {
var params = new URLSearchParams(window.location.search);
return params.get('campaign') || '(not set)';
}
// Get cookie value
function() {
var match = document.cookie.match('(^|;) ?user_id=([^;]*)(;|$)');
return match ? match[2] : null;
}
// Get data from page
function() {
var el = document.querySelector('.product-price');
return el ? parseFloat(el.textContent.replace('$', '')) : 0;
}
```

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/trailofbits/skills/tree/main/plugins/ask-questions-if-underspecified/skills/ask-questions-if-underspecified",
"type": "github-subdir",
"installed_at": "2026-01-30T02:23:15.701719296Z",
"repo_url": "https://github.com/trailofbits/skills.git",
"subdir": "plugins/ask-questions-if-underspecified/skills/ask-questions-if-underspecified",
"version": "650f6e3"
}

View File

@@ -0,0 +1,85 @@
---
name: ask-questions-if-underspecified
description: Clarify requirements before implementing. Use when serious doubts arise.
---
# Ask Questions If Underspecified
## When to Use
Use this skill when a request has multiple plausible interpretations or key details (objective, scope, constraints, environment, or safety) are unclear.
## When NOT to Use
Do not use this skill when the request is already clear, or when a quick, low-risk discovery read can answer the missing details.
## Goal
Ask the minimum set of clarifying questions needed to avoid wrong work; do not start implementing until the must-have questions are answered (or the user explicitly approves proceeding with stated assumptions).
## Workflow
### 1) Decide whether the request is underspecified
Treat a request as underspecified if after exploring how to perform the work, some or all of the following are not clear:
- Define the objective (what should change vs stay the same)
- Define "done" (acceptance criteria, examples, edge cases)
- Define scope (which files/components/users are in/out)
- Define constraints (compatibility, performance, style, deps, time)
- Identify environment (language/runtime versions, OS, build/test runner)
- Clarify safety/reversibility (data migration, rollout/rollback, risk)
If multiple plausible interpretations exist, assume it is underspecified.
### 2) Ask must-have questions first (keep it small)
Ask 1-5 questions in the first pass. Prefer questions that eliminate whole branches of work.
Make questions easy to answer:
- Optimize for scannability (short, numbered questions; avoid paragraphs)
- Offer multiple-choice options when possible
- Suggest reasonable defaults when appropriate (mark them clearly as the default/recommended choice; bold the recommended choice in the list, or if you present options in a code block, put a bold "Recommended" line immediately above the block and also tag defaults inside the block)
- Include a fast-path response (e.g., reply `defaults` to accept all recommended/default choices)
- Include a low-friction "not sure" option when helpful (e.g., "Not sure - use default")
- Separate "Need to know" from "Nice to know" if that reduces friction
- Structure options so the user can respond with compact decisions (e.g., `1b 2a 3c`); restate the chosen options in plain language to confirm
### 3) Pause before acting
Until must-have answers arrive:
- Do not run commands, edit files, or produce a detailed plan that depends on unknowns
- Do perform a clearly labeled, low-risk discovery step only if it does not commit you to a direction (e.g., inspect repo structure, read relevant config files)
If the user explicitly asks you to proceed without answers:
- State your assumptions as a short numbered list
- Ask for confirmation; proceed only after they confirm or correct them
### 4) Confirm interpretation, then proceed
Once you have answers, restate the requirements in 1-3 sentences (including key constraints and what success looks like), then start work.
## Question templates
- "Before I start, I need: (1) ..., (2) ..., (3) .... If you don't care about (2), I will assume ...."
- "Which of these should it be? A) ... B) ... C) ... (pick one)"
- "What would you consider 'done'? For example: ..."
- "Any constraints I must follow (versions, performance, style, deps)? If none, I will target the existing project defaults."
- Use numbered questions with lettered options and a clear reply format
```text
1) Scope?
a) Minimal change (default)
b) Refactor while touching the area
c) Not sure - use default
2) Compatibility target?
a) Current project defaults (default)
b) Also support older versions: <specify>
c) Not sure - use default
Reply with: defaults (or 1a 2a)
```
## Anti-patterns
- Don't ask questions you can answer with a quick, low-risk discovery read (e.g., configs, existing patterns, docs).
- Don't ask open-ended questions if a tight multiple-choice or yes/no would eliminate ambiguity faster.

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/trailofbits/skills/tree/main/plugins/audit-context-building/skills/audit-context-building",
"type": "github-subdir",
"installed_at": "2026-01-30T02:23:12.950780406Z",
"repo_url": "https://github.com/trailofbits/skills.git",
"subdir": "plugins/audit-context-building/skills/audit-context-building",
"version": "650f6e3"
}

View File

@@ -0,0 +1,297 @@
---
name: audit-context-building
description: Enables ultra-granular, line-by-line code analysis to build deep architectural context before vulnerability or bug finding.
---
# Deep Context Builder Skill (Ultra-Granular Pure Context Mode)
## 1. Purpose
This skill governs **how Claude thinks** during the context-building phase of an audit.
When active, Claude will:
- Perform **line-by-line / block-by-block** code analysis by default.
- Apply **First Principles**, **5 Whys**, and **5 Hows** at micro scale.
- Continuously link insights → functions → modules → entire system.
- Maintain a stable, explicit mental model that evolves with new evidence.
- Identify invariants, assumptions, flows, and reasoning hazards.
This skill defines a structured analysis format (see Example: Function Micro-Analysis below) and runs **before** the vulnerability-hunting phase.
---
## 2. When to Use This Skill
Use when:
- Deep comprehension is needed before bug or vulnerability discovery.
- You want bottom-up understanding instead of high-level guessing.
- Reducing hallucinations, contradictions, and context loss is critical.
- Preparing for security auditing, architecture review, or threat modeling.
Do **not** use for:
- Vulnerability findings
- Fix recommendations
- Exploit reasoning
- Severity/impact rating
---
## 3. How This Skill Behaves
When active, Claude will:
- Default to **ultra-granular analysis** of each block and line.
- Apply micro-level First Principles, 5 Whys, and 5 Hows.
- Build and refine a persistent global mental model.
- Update earlier assumptions when contradicted ("Earlier I thought X; now Y.").
- Periodically anchor summaries to maintain stable context.
- Avoid speculation; express uncertainty explicitly when needed.
Goal: **deep, accurate understanding**, not conclusions.
---
## Rationalizations (Do Not Skip)
| Rationalization | Why It's Wrong | Required Action |
|-----------------|----------------|-----------------|
| "I get the gist" | Gist-level understanding misses edge cases | Line-by-line analysis required |
| "This function is simple" | Simple functions compose into complex bugs | Apply 5 Whys anyway |
| "I'll remember this invariant" | You won't. Context degrades. | Write it down explicitly |
| "External call is probably fine" | External = adversarial until proven otherwise | Jump into code or model as hostile |
| "I can skip this helper" | Helpers contain assumptions that propagate | Trace the full call chain |
| "This is taking too long" | Rushed context = hallucinated vulnerabilities later | Slow is fast |
---
## 4. Phase 1 — Initial Orientation (Bottom-Up Scan)
Before deep analysis, Claude performs a minimal mapping:
1. Identify major modules/files/contracts.
2. Note obvious public/external entrypoints.
3. Identify likely actors (users, owners, relayers, oracles, other contracts).
4. Identify important storage variables, dicts, state structs, or cells.
5. Build a preliminary structure without assuming behavior.
This establishes anchors for detailed analysis.
---
## 5. Phase 2 — Ultra-Granular Function Analysis (Default Mode)
Every non-trivial function receives full micro analysis.
### 5.1 Per-Function Microstructure Checklist
For each function:
1. **Purpose**
- Why the function exists and its role in the system.
2. **Inputs & Assumptions**
- Parameters and implicit inputs (state, sender, env).
- Preconditions and constraints.
3. **Outputs & Effects**
- Return values.
- State/storage writes.
- Events/messages.
- External interactions.
4. **Block-by-Block / Line-by-Line Analysis**
For each logical block:
- What it does.
- Why it appears here (ordering logic).
- What assumptions it relies on.
- What invariants it establishes or maintains.
- What later logic depends on it.
Apply per-block:
- **First Principles**
- **5 Whys**
- **5 Hows**
---
### 5.2 Cross-Function & External Flow Analysis
*(Full Integration of Jump-Into-External-Code Rule)*
When encountering calls, **continue the same micro-first analysis across boundaries.**
#### Internal Calls
- Jump into the callee immediately.
- Perform block-by-block analysis of relevant code.
- Track flow of data, assumptions, and invariants:
caller → callee → return → caller.
- Note if callee logic behaves differently in this specific call context.
#### External Calls — Two Cases
**Case A — External Call to a Contract Whose Code Exists in the Codebase**
Treat as an internal call:
- Jump into the target contract/function.
- Continue block-by-block micro-analysis.
- Propagate invariants and assumptions seamlessly.
- Consider edge cases based on the *actual* code, not a black-box guess.
**Case B — External Call Without Available Code (True External / Black Box)**
Analyze as adversarial:
- Describe payload/value/gas or parameters sent.
- Identify assumptions about the target.
- Consider all outcomes:
- revert
- incorrect/strange return values
- unexpected state changes
- misbehavior
- reentrancy (if applicable)
#### Continuity Rule
Treat the entire call chain as **one continuous execution flow**.
Never reset context.
All invariants, assumptions, and data dependencies must propagate across calls.
---
### 5.3 Complete Analysis Example
See [FUNCTION_MICRO_ANALYSIS_EXAMPLE.md](resources/FUNCTION_MICRO_ANALYSIS_EXAMPLE.md) for a complete walkthrough demonstrating:
- Full micro-analysis of a DEX swap function
- Application of First Principles, 5 Whys, and 5 Hows
- Block-by-block analysis with invariants and assumptions
- Cross-function dependency mapping
- Risk analysis for external interactions
This example demonstrates the level of depth and structure required for all analyzed functions.
---
### 5.4 Output Requirements
When performing ultra-granular analysis, Claude MUST structure output following the format defined in [OUTPUT_REQUIREMENTS.md](resources/OUTPUT_REQUIREMENTS.md).
Key requirements:
- **Purpose** (2-3 sentences minimum)
- **Inputs & Assumptions** (all parameters, preconditions, trust assumptions)
- **Outputs & Effects** (returns, state writes, external calls, events, postconditions)
- **Block-by-Block Analysis** (What, Why here, Assumptions, First Principles/5 Whys/5 Hows)
- **Cross-Function Dependencies** (internal calls, external calls with risk analysis, shared state)
Quality thresholds:
- Minimum 3 invariants per function
- Minimum 5 assumptions documented
- Minimum 3 risk considerations for external interactions
- At least 1 First Principles application
- At least 3 combined 5 Whys/5 Hows applications
---
### 5.5 Completeness Checklist
Before concluding micro-analysis of a function, verify against the [COMPLETENESS_CHECKLIST.md](resources/COMPLETENESS_CHECKLIST.md):
- **Structural Completeness**: All required sections present (Purpose, Inputs, Outputs, Block-by-Block, Dependencies)
- **Content Depth**: Minimum thresholds met (invariants, assumptions, risk analysis, First Principles)
- **Continuity & Integration**: Cross-references, propagated assumptions, invariant couplings
- **Anti-Hallucination**: Line number citations, no vague statements, evidence-based claims
Analysis is complete when all checklist items are satisfied and no unresolved "unclear" items remain.
---
## 6. Phase 3 — Global System Understanding
After sufficient micro-analysis:
1. **State & Invariant Reconstruction**
- Map reads/writes of each state variable.
- Derive multi-function and multi-module invariants.
2. **Workflow Reconstruction**
- Identify end-to-end flows (deposit, withdraw, lifecycle, upgrades).
- Track how state transforms across these flows.
- Record assumptions that persist across steps.
3. **Trust Boundary Mapping**
- Actor → entrypoint → behavior.
- Identify untrusted input paths.
- Privilege changes and implicit role expectations.
4. **Complexity & Fragility Clustering**
- Functions with many assumptions.
- High branching logic.
- Multi-step dependencies.
- Coupled state changes across modules.
These clusters help guide the vulnerability-hunting phase.
---
## 7. Stability & Consistency Rules
*(Anti-Hallucination, Anti-Contradiction)*
Claude must:
- **Never reshape evidence to fit earlier assumptions.**
When contradicted:
- Update the model.
- State the correction explicitly.
- **Periodically anchor key facts**
Summarize core:
- invariants
- state relationships
- actor roles
- workflows
- **Avoid vague guesses**
Use:
- "Unclear; need to inspect X."
instead of:
- "It probably…"
- **Cross-reference constantly**
Connect new insights to previous state, flows, and invariants to maintain global coherence.
---
## 8. Subagent Usage
Claude may spawn subagents for:
- Dense or complex functions.
- Long data-flow or control-flow chains.
- Cryptographic / mathematical logic.
- Complex state machines.
- Multi-module workflow reconstruction.
Subagents must:
- Follow the same micro-first rules.
- Return summaries that Claude integrates into its global model.
---
## 9. Relationship to Other Phases
This skill runs **before**:
- Vulnerability discovery
- Classification / triage
- Report writing
- Impact modeling
- Exploit reasoning
It exists solely to build:
- Deep understanding
- Stable context
- System-level clarity
---
## 10. Non-Goals
While active, Claude should NOT:
- Identify vulnerabilities
- Propose fixes
- Generate proofs-of-concept
- Model exploits
- Assign severity or impact
This is **pure context building** only.

View File

@@ -0,0 +1,47 @@
# Completeness Checklist
Before concluding micro-analysis of a function, verify:
---
## Structural Completeness
- [ ] Purpose section: 2+ sentences explaining function role
- [ ] Inputs & Assumptions section: All parameters + implicit inputs documented
- [ ] Outputs & Effects section: All returns, state writes, external calls, events
- [ ] Block-by-Block Analysis: Every logical block analyzed (no gaps)
- [ ] Cross-Function Dependencies: All calls and shared state documented
---
## Content Depth
- [ ] Identified at least 3 invariants (what must always hold)
- [ ] Documented at least 5 assumptions (what is assumed true)
- [ ] Applied First Principles at least once
- [ ] Applied 5 Whys or 5 Hows at least 3 times total
- [ ] Risk analysis for all external interactions (reentrancy, malicious contracts, etc.)
---
## Continuity & Integration
- [ ] Cross-reference with related functions (if internal calls exist, analyze callees)
- [ ] Propagated assumptions from callers (if this function is called by others)
- [ ] Identified invariant couplings (how this function's invariants relate to global system)
- [ ] Tracked data flow across function boundaries (if applicable)
---
## Anti-Hallucination Verification
- [ ] All claims reference specific line numbers (L45, L98-102, etc.)
- [ ] No vague statements ("probably", "might", "seems to") - replaced with "unclear; need to check X"
- [ ] Contradictions resolved (if earlier analysis conflicts with current findings, explicitly updated)
- [ ] Evidence-based: Every invariant/assumption tied to actual code
---
## Completeness Signal
Analysis is complete when:
1. All checklist items above are satisfied
2. No remaining "TODO: analyze X" or "unclear Y" items
3. Full call chain analyzed (for internal calls, jumped into and analyzed)
4. All identified risks have mitigation analysis or acknowledged as unresolved

View File

@@ -0,0 +1,355 @@
# Function Micro-Analysis Example
This example demonstrates a complete micro-analysis following the Per-Function Microstructure Checklist.
---
## Target: `swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, uint256 deadline)` in Router.sol
**Purpose:**
Enables users to swap one token for another through a liquidity pool. Core trading operation in a DEX that:
- Calculates output amount using constant product formula (x * y = k)
- Deducts 0.3% protocol fee from input amount
- Enforces user-specified slippage protection
- Updates pool reserves to maintain AMM invariant
- Prevents stale transactions via deadline check
This is a critical financial primitive affecting pool solvency, user fund safety, and protocol fee collection.
---
**Inputs & Assumptions:**
*Parameters:*
- `tokenIn` (address): Source token to swap from. Assumed untrusted (could be malicious ERC20).
- `tokenOut` (address): Destination token to receive. Assumed untrusted.
- `amountIn` (uint256): Amount of tokenIn to swap. User-specified, untrusted input.
- `minAmountOut` (uint256): Minimum acceptable output. User-specified slippage tolerance.
- `deadline` (uint256): Unix timestamp. Transaction must execute before this or revert.
*Implicit Inputs:*
- `msg.sender`: Transaction initiator. Assumed to have approved Router to spend amountIn of tokenIn.
- `pairs[tokenIn][tokenOut]`: Storage mapping to pool address. Assumed populated during pool creation.
- `reserves[pair]`: Pool's current token reserves. Assumed synchronized with actual pool balances.
- `block.timestamp`: Current block time. Assumed honest (no validator manipulation considered here).
*Preconditions:*
- Pool exists for tokenIn/tokenOut pair (pairs[tokenIn][tokenOut] != address(0))
- msg.sender has approved Router for at least amountIn of tokenIn
- msg.sender balance of tokenIn >= amountIn
- Pool has sufficient liquidity to output at least minAmountOut
- block.timestamp <= deadline
*Trust Assumptions:*
- Pool contract correctly maintains reserves
- ERC20 tokens follow standard behavior (return true on success, revert on failure)
- No reentrancy from tokenIn/tokenOut during transfers (or handled by nonReentrant modifier)
---
**Outputs & Effects:**
*Returns:*
- Implicit: amountOut (not returned, but emitted in event)
*State Writes:*
- `reserves[pair].reserve0` and `reserves[pair].reserve1`: Updated to reflect post-swap balances
- Pool token balances: Physical token transfers change actual balances
*External Interactions:*
- `IERC20(tokenIn).transferFrom(msg.sender, pair, amountIn)`: Pulls tokenIn from user to pool
- `IERC20(tokenOut).transfer(msg.sender, amountOut)`: Sends tokenOut from pool to user
*Events Emitted:*
- `Swap(msg.sender, tokenIn, tokenOut, amountIn, amountOut, block.timestamp)`
*Postconditions:*
- `amountOut >= minAmountOut` (slippage protection enforced)
- Pool reserves updated: `reserve0 * reserve1 >= k_before` (constant product maintained with fee)
- User received exactly amountOut of tokenOut
- Pool received exactly amountIn of tokenIn
- Fee collected: `amountIn * 0.003` remains in pool as liquidity
---
**Block-by-Block Analysis:**
```solidity
// L90: Deadline validation (modifier: ensure(deadline))
modifier ensure(uint256 deadline) {
require(block.timestamp <= deadline, "Expired");
_;
}
```
- **What:** Checks transaction hasn't expired based on user-provided deadline
- **Why here:** First line of defense; fail fast before any state reads or computation
- **Assumption:** `block.timestamp` is sufficiently honest (no 900-second manipulation considered)
- **Depends on:** User setting reasonable deadline (e.g., block.timestamp + 300 seconds)
- **First Principles:** Time-sensitive operations need expiration to prevent stale execution at unexpected prices
- **5 Whys:**
- Why check deadline? → Prevent stale transactions
- Why are stale transactions bad? → Price may have moved significantly
- Why not just use slippage protection? → Slippage doesn't prevent execution hours later
- Why does timing matter? → Market conditions change, user intent expires
- Why user-provided vs fixed? → User decides their time tolerance based on urgency
---
```solidity
// L92-94: Input validation
require(amountIn > 0, "Invalid input amount");
require(minAmountOut > 0, "Invalid minimum output");
require(tokenIn != tokenOut, "Identical tokens");
```
- **What:** Validates basic input sanity (non-zero amounts, different tokens)
- **Why here:** Second line of defense; cheap checks before expensive operations
- **Assumption:** Zero amounts indicate user error, not intentional probe
- **Invariant established:** `amountIn > 0 && minAmountOut > 0 && tokenIn != tokenOut`
- **First Principles:** Fail fast on invalid input before consuming gas on computation/storage
- **5 Hows:**
- How to ensure valid swap? → Check inputs meet minimum requirements
- How to check minimum requirements? → Test amounts > 0 and tokens differ
- How to handle violations? → Revert with descriptive error
- How to order checks? → Cheapest first (inequality checks before storage reads)
- How to communicate failure? → Require statements with clear messages
---
```solidity
// L98-99: Pool resolution
address pair = pairs[tokenIn][tokenOut];
require(pair != address(0), "Pool does not exist");
```
- **What:** Looks up liquidity pool address for token pair, validates existence
- **Why here:** Must identify pool before reading reserves or executing transfers
- **Assumption:** `pairs` mapping is correctly populated during pool creation; no race conditions
- **Depends on:** Factory having called createPair(tokenIn, tokenOut) previously
- **Invariant established:** `pair != 0x0` (valid pool address exists)
- **Risk:** If pairs mapping is corrupted or pool address is incorrect, funds could be sent to wrong address
---
```solidity
// L102-103: Reserve reads
(uint112 reserveIn, uint112 reserveOut) = getReserves(pair, tokenIn, tokenOut);
require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");
```
- **What:** Reads current pool reserves for tokenIn and tokenOut, validates pool has liquidity
- **Why here:** Need current reserves to calculate output amount; must confirm pool is operational
- **Assumption:** `reserves[pair]` storage is synchronized with actual pool token balances
- **Invariant established:** `reserveIn > 0 && reserveOut > 0` (pool is liquid)
- **Depends on:** Sync mechanism keeping reserves accurate (called after transfers/swaps)
- **5 Whys:**
- Why read reserves? → Need current pool state for price calculation
- Why must reserves be > 0? → Division by zero in formula if empty
- Why check liquidity here? → Cheaper to fail now than after transferFrom
- Why not just try the swap? → Better UX with specific error message
- Why trust reserves storage? → Alternative is querying balances (expensive)
---
```solidity
// L108-109: Fee application
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
```
- **What:** Applies 0.3% protocol fee by multiplying amountIn by 997 (instead of deducting 3)
- **Why here:** Fee must be applied before price calculation to affect output amount
- **Assumption:** 997/1000 = 0.997 = (1 - 0.003) represents 0.3% fee deduction
- **Invariant maintained:** `amountInWithFee = amountIn * 0.997` (3/1000 fee taken)
- **First Principles:** Fees modify effective input, reducing output proportionally
- **5 Whys:**
- Why multiply by 997? → Gas optimization: avoids separate subtraction step
- Why not amountIn * 0.997? → Solidity doesn't support floating point
- Why 0.3% fee? → Protocol parameter (Uniswap V2 standard, commonly copied)
- Why apply before calculation? → Fee reduces input amount, must affect price
- Why not apply after? → Would incorrectly calculate output at full amountIn
---
```solidity
// L110-111: Output calculation (constant product formula)
uint256 denominator = (reserveIn * 1000) + amountInWithFee;
uint256 amountOut = numerator / denominator;
```
- **What:** Calculates output amount using AMM constant product formula: `Δy = (x * Δx_fee) / (y + Δx_fee)`
- **Why here:** After fee application; core pricing logic of the AMM
- **Assumption:** `k = reserveIn * reserveOut` is the invariant to maintain (with fee adding to k)
- **Invariant formula:** `(reserveIn + amountIn) * (reserveOut - amountOut) >= reserveIn * reserveOut`
- **First Principles:** Constant product AMM maintains `x * y = k` (with fee slightly increasing k)
- **5 Whys:**
- Why this formula? → Constant product market maker (x * y = k)
- Why not linear pricing? → Would drain pool at constant price (exploitable)
- Why multiply reserveIn by 1000? → Match denominator scale with numerator (997 * 1000)
- Why divide? → Solving for Δy in: (x + Δx_fee) * (y - Δy) = k
- Why this maintains k? → New product = (reserveIn + amountIn*0.997) * (reserveOut - amountOut) ≈ k * 1.003
- **Mathematical verification:**
- Given: `k = reserveIn * reserveOut`
- New reserves: `reserveIn' = reserveIn + amountIn`, `reserveOut' = reserveOut - amountOut`
- With fee: `amountInWithFee = amountIn * 0.997`
- Solving `(reserveIn + amountIn) * (reserveOut - amountOut) = k`:
- `reserveOut - amountOut = k / (reserveIn + amountIn)`
- `amountOut = reserveOut - k / (reserveIn + amountIn)`
- Substituting and simplifying yields the formula above
---
```solidity
// L115: Slippage protection enforcement
require(amountOut >= minAmountOut, "Slippage exceeded");
```
- **What:** Validates calculated output meets user's minimum acceptable amount
- **Why here:** After calculation, before any state changes or transfers (fail fast if insufficient)
- **Assumption:** User calculated minAmountOut correctly based on acceptable slippage tolerance
- **Invariant enforced:** `amountOut >= minAmountOut` (user-defined slippage limit)
- **First Principles:** User must explicitly consent to price via slippage tolerance; prevents sandwich attacks
- **5 Whys:**
- Why check minAmountOut? → Protect user from excessive slippage
- Why is slippage protection critical? → Prevents sandwich attacks and MEV extraction
- Why user-specified? → Different users have different risk tolerances
- Why fail here vs warn? → Financial safety: user should not receive less than intended
- Why before transfers? → Cheaper to revert now than after expensive external calls
- **Attack scenario prevented:**
- Attacker front-runs with large buy → price increases
- Victim's swap would execute at worse price
- This check causes victim's transaction to revert instead
- Attacker cannot profit from sandwich
---
```solidity
// L118: Input token transfer (pull pattern)
IERC20(tokenIn).transferFrom(msg.sender, pair, amountIn);
```
- **What:** Pulls tokenIn from user to liquidity pool
- **Why here:** After all validations pass; begins state-changing operations (point of no return)
- **Assumption:** User has approved Router for at least amountIn; tokenIn is standard ERC20
- **Depends on:** Prior approval: `tokenIn.approve(router, amountIn)` called by user
- **Risk considerations:**
- If tokenIn is malicious: could revert (DoS), consume excessive gas, or attempt reentrancy
- If tokenIn has transfer fee: actual amount received < amountIn (breaks invariant)
- If tokenIn is pausable: could revert if paused
- Reentrancy: If tokenIn has callback, attacker could call Router again (mitigated by nonReentrant modifier)
- **First Principles:** Pull pattern (transferFrom) is safer than users sending first (push) - Router controls timing
- **5 Hows:**
- How to get tokenIn? → Pull from user via transferFrom
- How to ensure Router can pull? → User must have approved Router
- How to specify destination? → Send directly to pair (gas optimization: no router intermediate storage)
- How to handle failures? → transferFrom reverts on failure (ERC20 standard)
- How to prevent reentrancy? → nonReentrant modifier (assumed present)
---
```solidity
// L122: Output token transfer (push pattern)
IERC20(tokenOut).transfer(msg.sender, amountOut);
```
- **What:** Sends calculated amountOut of tokenOut from pool to user
- **Why here:** After input transfer succeeds; completes the swap atomically
- **Assumption:** Pool has at least amountOut of tokenOut; tokenOut is standard ERC20
- **Invariant maintained:** User receives exact amountOut (no more, no less)
- **Risk considerations:**
- If tokenOut is malicious: could revert (DoS), but user selected this token pair
- If tokenOut has transfer hook: could attempt reentrancy (mitigated by nonReentrant)
- If transfer fails: entire transaction reverts (atomic swap)
- **CEI pattern:** Not strictly followed (Check-Effects-Interactions) - both transfers are interactions
- Typically Effects (reserve update) should precede Interactions (transfers)
- Here, transfers happen before reserve update (see next block)
- Justification: nonReentrant modifier prevents exploitation
- **5 Whys:**
- Why transfer to msg.sender? → User initiated swap, they receive output
- Why not to an arbitrary recipient? → Simplicity; extensions can add recipient parameter
- Why this amount exactly? → amountOut calculated from constant product formula
- Why after input transfer? → Ensures atomicity: both succeed or both fail
- Why trust pool has balance? → Pool's job to maintain reserves; if insufficient, transfer reverts
---
```solidity
// L125-126: Reserve synchronization
reserves[pair].reserve0 = uint112(reserveIn + amountIn);
reserves[pair].reserve1 = uint112(reserveOut - amountOut);
```
- **What:** Updates stored reserves to reflect post-swap balances
- **Why here:** After transfers complete; brings storage in sync with actual balances
- **Assumption:** No other operations have modified pool balances since reserves were read
- **Invariant maintained:** `reserve0 * reserve1 >= k_before * 1.003` (constant product + fee)
- **Casting risk:** `uint112` casting could truncate if reserves exceed 2^112 - 1 (≈ 5.2e33)
- For most tokens with 18 decimals: limit is ~5.2e15 tokens
- Overflow protection: require reserves fit in uint112, else revert
- **5 Whys:**
- Why update reserves? → Storage must match actual balances for next swap
- Why after transfers? → Need to know final state before recording
- Why not query balances? → Gas optimization: storage update cheaper than CALL + BALANCE
- Why uint112? → Pack two reserves in one storage slot (256 bits = 2 * 112 + 32 for timestamp)
- Why this formula? → reserveIn increased by amountIn, reserveOut decreased by amountOut
- **Invariant verification:**
- Before: `k_before = reserveIn * reserveOut`
- After: `k_after = (reserveIn + amountIn) * (reserveOut - amountOut)`
- With 0.3% fee: `k_after ≈ k_before * 1.003` (fee adds permanent liquidity)
---
```solidity
// L130: Event emission
emit Swap(msg.sender, tokenIn, tokenOut, amountIn, amountOut, block.timestamp);
```
- **What:** Emits event logging swap details for off-chain indexing
- **Why here:** After all state changes finalized; last operation before return
- **Assumption:** Event watchers (subgraphs, dex aggregators) rely on this for tracking trades
- **Data included:**
- `msg.sender`: Who initiated swap (for user trade history)
- `tokenIn/tokenOut`: Which pair was traded
- `amountIn/amountOut`: Exact amounts for price tracking
- `block.timestamp`: When trade occurred (for TWAP calculations, analytics)
- **First Principles:** Events are write-only log for off-chain systems; don't affect on-chain state
- **5 Hows:**
- How to notify off-chain? → Emit event (logs are cheaper than storage)
- How to structure event? → Include all relevant swap parameters
- How do indexers use this? → Build trade history, calculate volume, track prices
- How to ensure consistency? → Emit after state finalized (can't be front-run)
- How to query later? → Blockchain logs filtered by event signature + contract address
---
**Cross-Function Dependencies:**
*Internal Calls:*
- `getReserves(pair, tokenIn, tokenOut)`: Helper to read and order reserves based on token addresses
- Depends on: `reserves[pair]` storage being synchronized
- Returns: (reserveIn, reserveOut) in correct order for tokenIn/tokenOut
*External Calls (Outbound):*
- `IERC20(tokenIn).transferFrom(msg.sender, pair, amountIn)`: ERC20 standard call
- Assumes: tokenIn implements ERC20, user has approved Router
- Reentrancy risk: If tokenIn is malicious, could callback
- Failure: Reverts entire transaction
- `IERC20(tokenOut).transfer(msg.sender, amountOut)`: ERC20 standard call
- Assumes: Pool has sufficient tokenOut balance
- Reentrancy risk: If tokenOut has hooks
- Failure: Reverts entire transaction
*Called By:*
- Users directly (external call)
- Aggregators/routers (external call)
- Multi-hop swap functions (internal call from same contract)
*Shares State With:*
- `addLiquidity()`: Modifies same reserves[pair], must maintain k invariant
- `removeLiquidity()`: Modifies same reserves[pair]
- `sync()`: Emergency function to force reserves sync with balances
- `skim()`: Removes excess tokens beyond reserves
*Invariant Coupling:*
- **Global invariant:** `sum(all reserves[pair].reserve0 for all pairs) <= sum(all token balances in pools)`
- **Per-pool invariant:** `reserves[pair].reserve0 * reserves[pair].reserve1 >= k_initial * (1.003^n)` where n = number of swaps
- Each swap increases k by 0.3% due to fee
- **Reentrancy protection:** `nonReentrant` modifier ensures no cross-function reentrancy
- swap() cannot be re-entered while executing
- addLiquidity/removeLiquidity also cannot execute during swap
*Assumptions Propagated to Callers:*
- Caller must have approved Router to spend amountIn of tokenIn
- Caller must set reasonable deadline (e.g., block.timestamp + 300 seconds)
- Caller must calculate minAmountOut based on acceptable slippage (e.g., expectedOutput * 0.99 for 1%)
- Caller assumes pair exists (or will handle "Pool does not exist" revert)

View File

@@ -0,0 +1,71 @@
# Output Requirements
When performing ultra-granular analysis, Claude MUST structure output following the Per-Function Microstructure Checklist format demonstrated in [FUNCTION_MICRO_ANALYSIS_EXAMPLE.md](FUNCTION_MICRO_ANALYSIS_EXAMPLE.md).
---
## Required Structure
For EACH analyzed function, output MUST include:
**1. Purpose** (mandatory)
- Clear statement of function's role in the system
- Impact on system state, security, or economics
- Minimum 2-3 sentences
**2. Inputs & Assumptions** (mandatory)
- All parameters (explicit and implicit)
- All preconditions
- All trust assumptions
- Each input must identify: type, source, trust level
- Minimum 3 assumptions documented
**3. Outputs & Effects** (mandatory)
- Return values (or "void" if none)
- All state writes
- All external interactions
- All events emitted
- All postconditions
- Minimum 3 effects documented
**4. Block-by-Block Analysis** (mandatory)
For EACH logical code block, document:
- **What:** What the block does (1 sentence)
- **Why here:** Why this ordering/placement (1 sentence)
- **Assumptions:** What must be true (1+ items)
- **Depends on:** What prior state/logic this relies on
- **First Principles / 5 Whys / 5 Hows:** Apply at least ONE per block
Minimum standards:
- Analyze at minimum: ALL conditional branches, ALL external calls, ALL state modifications
- For complex blocks (>5 lines): Apply First Principles AND 5 Whys or 5 Hows
- For simple blocks (<5 lines): Minimum What + Why here + 1 Assumption
**5. Cross-Function Dependencies** (mandatory)
- Internal calls made (list all)
- External calls made (list all with risk analysis)
- Functions that call this function
- Shared state with other functions
- Invariant couplings (how this function's invariants interact with others)
- Minimum 3 dependency relationships documented
---
## Quality Thresholds
A complete micro-analysis MUST identify:
- Minimum 3 invariants (per function)
- Minimum 5 assumptions (across all sections)
- Minimum 3 risk considerations (especially for external interactions)
- At least 1 application of First Principles
- At least 3 applications of 5 Whys or 5 Hows (combined)
---
## Format Consistency
- Use markdown headers: `**Section Name:**` for major sections
- Use bullet points (`-`) for lists
- Use code blocks (` ```solidity `) for code snippets
- Reference line numbers: `L45`, `lines 98-102`
- Separate blocks with `---` horizontal rules for readability

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/trailofbits/skills/tree/main/plugins/building-secure-contracts/skills/audit-prep-assistant",
"type": "github-subdir",
"installed_at": "2026-01-30T02:24:00.994207953Z",
"repo_url": "https://github.com/trailofbits/skills.git",
"subdir": "plugins/building-secure-contracts/skills/audit-prep-assistant",
"version": "650f6e3"
}

View File

@@ -0,0 +1,409 @@
---
name: audit-prep-assistant
description: Prepares codebases for security review using Trail of Bits' checklist. Helps set review goals, runs static analysis tools, increases test coverage, removes dead code, ensures accessibility, and generates documentation (flowcharts, user stories, inline comments).
---
# Audit Prep Assistant
## Purpose
Helps prepare for a security review using Trail of Bits' checklist. A well-prepared codebase makes the review process smoother and more effective.
**Use this**: 1-2 weeks before your security audit
---
## The Preparation Process
### Step 1: Set Review Goals
Helps define what you want from the review:
**Key Questions**:
- What's the overall security level you're aiming for?
- What areas concern you most?
- Previous audit issues?
- Complex components?
- Fragile parts?
- What's the worst-case scenario for your project?
Documents goals to share with the assessment team.
---
### Step 2: Resolve Easy Issues
Runs static analysis and helps fix low-hanging fruit:
**Run Static Analysis**:
For Solidity:
```bash
slither . --exclude-dependencies
```
For Rust:
```bash
dylint --all
```
For Go:
```bash
golangci-lint run
```
For Go/Rust/C++:
```bash
# CodeQL and Semgrep checks
```
Then I'll:
- Triage all findings
- Help fix easy issues
- Document accepted risks
**Increase Test Coverage**:
- Analyze current coverage
- Identify untested code
- Suggest new tests
- Run full test suite
**Remove Dead Code**:
- Find unused functions/variables
- Identify unused libraries
- Locate stale features
- Suggest cleanup
**Goal**: Clean static analysis report, high test coverage, minimal dead code
---
### Step 3: Ensure Code Accessibility
Helps make code clear and accessible:
**Provide Detailed File List**:
- List all files in scope
- Mark out-of-scope files
- Explain folder structure
- Document dependencies
**Create Build Instructions**:
- Write step-by-step setup guide
- Test on fresh environment
- Document dependencies and versions
- Verify build succeeds
**Freeze Stable Version**:
- Identify commit hash for review
- Create dedicated branch
- Tag release version
- Lock dependencies
**Identify Boilerplate**:
- Mark copied/forked code
- Highlight your modifications
- Document third-party code
- Focus review on your code
---
### Step 4: Generate Documentation
Helps create documentation:
**Flowcharts and Sequence Diagrams**:
- Map primary workflows
- Show component relationships
- Visualize data flow
- Identify critical paths
**User Stories**:
- Define user roles
- Document use cases
- Explain interactions
- Clarify expectations
**On-chain/Off-chain Assumptions**:
- Data validation procedures
- Oracle information
- Bridge assumptions
- Trust boundaries
**Actors and Privileges**:
- List all actors
- Document roles
- Define privileges
- Map access controls
**External Developer Docs**:
- Link docs to code
- Keep synchronized
- Explain architecture
- Document APIs
**Function Documentation**:
- System and function invariants
- Parameter ranges (min/max values)
- Arithmetic formulas and precision loss
- Complex logic explanations
- NatSpec for Solidity
**Glossary**:
- Define domain terms
- Explain acronyms
- Consistent terminology
- Business logic concepts
**Video Walkthroughs** (optional):
- Complex workflows
- Areas of concern
- Architecture overview
---
## How I Work
When invoked, I will:
1. **Help set review goals** - Ask about concerns and document them
2. **Run static analysis** - Execute appropriate tools for your platform
3. **Analyze test coverage** - Identify gaps and suggest improvements
4. **Find dead code** - Search for unused code and libraries
5. **Review accessibility** - Check build instructions and scope clarity
6. **Generate documentation** - Create flowcharts, user stories, glossaries
7. **Create prep checklist** - Track what's done and what's remaining
Adapts based on:
- Your platform (Solidity, Rust, Go, etc.)
- Available tools
- Existing documentation
- Review timeline
---
## Rationalizations (Do Not Skip)
| Rationalization | Why It's Wrong | Required Action |
|-----------------|----------------|-----------------|
| "README covers setup, no need for detailed build instructions" | READMEs assume context auditors don't have | Test build on fresh environment, document every dependency version |
| "Static analysis already ran, no need to run again" | Codebase changed since last run | Execute static analysis tools, generate fresh report |
| "Test coverage looks decent" | "Looks decent" isn't measured coverage | Run coverage tools, identify specific untested code paths |
| "Not much dead code to worry about" | Dead code hides during manual review | Use automated detection tools to find unused functions/variables |
| "Architecture is straightforward, no diagrams needed" | Text descriptions miss visual patterns | Generate actual flowcharts and sequence diagrams |
| "Can freeze version right before audit" | Last-minute freezing creates rushed handoff | Identify and document commit hash now, create dedicated branch |
| "Terms are self-explanatory" | Domain knowledge isn't universal | Create comprehensive glossary with all domain-specific terms |
| "I'll do this step later" | Steps build on each other - skipping creates gaps | Complete all 4 steps sequentially, track progress with checklist |
---
## Example Output
When I finish helping you prepare, you'll have concrete deliverables like:
```
=== AUDIT PREP PACKAGE ===
Project: DeFi DEX Protocol
Audit Date: March 15, 2024
Preparation Status: Complete
---
## REVIEW GOALS DOCUMENT
Security Objectives:
- Verify economic security of liquidity pool swaps
- Validate oracle manipulation resistance
- Assess flash loan attack vectors
Areas of Concern:
1. Complex AMM pricing calculation (src/SwapRouter.sol:89-156)
2. Multi-hop swap routing logic (src/Router.sol)
3. Oracle price aggregation (src/PriceOracle.sol:45-78)
Worst-Case Scenario:
- Flash loan attack drains liquidity pools via oracle manipulation
Questions for Auditors:
- Can the AMM pricing model produce negative slippage under edge cases?
- Is the slippage protection sufficient to prevent sandwich attacks?
- How resilient is the system to temporary oracle failures?
---
## STATIC ANALYSIS REPORT
Slither Scan Results:
✓ High: 0 issues
✓ Medium: 0 issues
⚠ Low: 2 issues (triaged - documented in TRIAGE.md)
Info: 5 issues (code style, acceptable)
Tool: slither . --exclude-dependencies
Date: March 1, 2024
Status: CLEAN (all critical issues resolved)
---
## TEST COVERAGE REPORT
Overall Coverage: 94%
- Statements: 1,245 / 1,321 (94%)
- Branches: 456 / 498 (92%)
- Functions: 89 / 92 (97%)
Uncovered Areas:
- Emergency pause admin functions (tested manually)
- Governance migration path (one-time use)
Command: forge coverage
Status: EXCELLENT
---
## CODE SCOPE
In-Scope Files (8):
✓ src/SwapRouter.sol (456 lines)
✓ src/LiquidityPool.sol (234 lines)
✓ src/PairFactory.sol (389 lines)
✓ src/PriceOracle.sol (167 lines)
✓ src/LiquidityManager.sol (298 lines)
✓ src/Governance.sol (201 lines)
✓ src/FlashLoan.sol (145 lines)
✓ src/RewardsDistributor.sol (178 lines)
Out-of-Scope:
- lib/ (OpenZeppelin, external dependencies)
- test/ (test contracts)
- scripts/ (deployment scripts)
Total In-Scope: 2,068 lines of Solidity
---
## BUILD INSTRUCTIONS
Prerequisites:
- Foundry 0.2.0+
- Node.js 18+
- Git
Setup:
```bash
git clone https://github.com/project/repo.git
cd repo
git checkout audit-march-2024 # Frozen branch
forge install
forge build
forge test
```
Verification:
✓ Build succeeds without errors
✓ All 127 tests pass
✓ No warnings from compiler
---
## DOCUMENTATION
Generated Artifacts:
✓ ARCHITECTURE.md - System overview with diagrams
✓ USER_STORIES.md - 12 user interaction flows
✓ GLOSSARY.md - 34 domain terms defined
✓ docs/diagrams/contract-interactions.png
✓ docs/diagrams/swap-flow.png
✓ docs/diagrams/state-machine.png
NatSpec Coverage: 100% of public functions
---
## DEPLOYMENT INFO
Network: Ethereum Mainnet
Commit: abc123def456 (audit-march-2024 branch)
Deployed Contracts:
- SwapRouter: 0x1234...
- PriceOracle: 0x5678...
[... etc]
---
PACKAGE READY FOR AUDIT ✓
Next Step: Share with Trail of Bits assessment team
```
---
## What You'll Get
**Review Goals Document**:
- Security objectives
- Areas of concern
- Worst-case scenarios
- Questions for auditors
**Clean Codebase**:
- Triaged static analysis (or clean report)
- High test coverage
- No dead code
- Clear scope
**Accessibility Package**:
- File list with scope
- Build instructions
- Frozen commit/branch
- Boilerplate identified
**Documentation Suite**:
- Flowcharts and diagrams
- User stories
- Architecture docs
- Actor/privilege map
- Inline code comments
- Glossary
- Video walkthroughs (if created)
**Audit Prep Checklist**:
- [ ] Review goals documented
- [ ] Static analysis clean/triaged
- [ ] Test coverage >80%
- [ ] Dead code removed
- [ ] Build instructions verified
- [ ] Stable version frozen
- [ ] Flowcharts created
- [ ] User stories documented
- [ ] Assumptions documented
- [ ] Actors/privileges listed
- [ ] Function docs complete
- [ ] Glossary created
---
## Timeline
**2 weeks before audit**:
- Set review goals
- Run static analysis
- Start fixing issues
**1 week before audit**:
- Increase test coverage
- Remove dead code
- Freeze stable version
- Start documentation
**Few days before audit**:
- Complete documentation
- Verify build instructions
- Create final checklist
- Send package to auditors
---
## Ready to Prep
Let me know when you're ready and I'll help you prepare for your security review!

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/better-auth/skills/tree/main/better-auth/best-practices",
"type": "github-subdir",
"installed_at": "2026-01-30T02:26:09.125899442Z",
"repo_url": "https://github.com/better-auth/skills.git",
"subdir": "better-auth/best-practices",
"version": "14c9623"
}

166
best-practices/SKILL.md Normal file
View File

@@ -0,0 +1,166 @@
---
name: better-auth-best-practices
description: Skill for integrating Better Auth - the comprehensive TypeScript authentication framework.
---
# Better Auth Integration Guide
**Always consult [better-auth.com/docs](https://better-auth.com/docs) for code examples and latest API.**
Better Auth is a TypeScript-first, framework-agnostic auth framework supporting email/password, OAuth, magic links, passkeys, and more via plugins.
---
## Quick Reference
### Environment Variables
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`)
Only define `baseURL`/`secret` in config if env vars are NOT set.
### File Location
CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
### CLI Commands
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter)
- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools
**Re-run after adding/changing plugins.**
---
## Core Config Options
| Option | Notes |
|--------|-------|
| `appName` | Optional display name |
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
| `basePath` | Default `/api/auth`. Set `/` for root. |
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
| `database` | Required for most features. See adapters docs. |
| `secondaryStorage` | Redis/KV for sessions & rate limits |
| `emailAndPassword` | `{ enabled: true }` to activate |
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
| `plugins` | Array of plugins |
| `trustedOrigins` | CSRF whitelist |
---
## Database
**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance.
**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`.
**Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`.
---
## Session Management
**Storage priority:**
1. If `secondaryStorage` defined → sessions go there (not DB)
2. Set `session.storeSessionInDatabase: true` to also persist to DB
3. No database + `cookieCache` → fully stateless mode
**Cookie cache strategies:**
- `compact` (default) - Base64url + HMAC. Smallest.
- `jwt` - Standard JWT. Readable but signed.
- `jwe` - Encrypted. Maximum security.
**Key options:** `session.expiresIn` (default 7 days), `session.updateAge` (refresh interval), `session.cookieCache.maxAge`, `session.cookieCache.version` (change to invalidate all sessions).
---
## User & Account Config
**User:** `user.modelName`, `user.fields` (column mapping), `user.additionalFields`, `user.changeEmail.enabled` (disabled by default), `user.deleteUser.enabled` (disabled by default).
**Account:** `account.modelName`, `account.accountLinking.enabled`, `account.storeAccountCookie` (for stateless OAuth).
**Required for registration:** `email` and `name` fields.
---
## Email Flows
- `emailVerification.sendVerificationEmail` - Must be defined for verification to work
- `emailVerification.sendOnSignUp` / `sendOnSignIn` - Auto-send triggers
- `emailAndPassword.sendResetPassword` - Password reset email handler
---
## Security
**In `advanced`:**
- `useSecureCookies` - Force HTTPS cookies
- `disableCSRFCheck` - ⚠️ Security risk
- `disableOriginCheck` - ⚠️ Security risk
- `crossSubDomainCookies.enabled` - Share cookies across subdomains
- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies
- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false`
**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage").
---
## Hooks
**Endpoint hooks:** `hooks.before` / `hooks.after` - Array of `{ matcher, handler }`. Use `createAuthMiddleware`. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`.
**Database hooks:** `databaseHooks.user.create.before/after`, same for `session`, `account`. Useful for adding default values or post-creation actions.
**Hook context (`ctx.context`):** `session`, `secret`, `authCookies`, `password.hash()`/`verify()`, `adapter`, `internalAdapter`, `generateId()`, `tables`, `baseURL`.
---
## Plugins
**Import from dedicated paths for tree-shaking:**
```
import { twoFactor } from "better-auth/plugins/two-factor"
```
NOT `from "better-auth/plugins"`.
**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`.
Client plugins go in `createAuthClient({ plugins: [...] })`.
---
## Client
Import from: `better-auth/client` (vanilla), `better-auth/react`, `better-auth/vue`, `better-auth/svelte`, `better-auth/solid`.
Key methods: `signUp.email()`, `signIn.email()`, `signIn.social()`, `signOut()`, `useSession()`, `getSession()`, `revokeSession()`, `revokeSessions()`.
---
## Type Safety
Infer types: `typeof auth.$Infer.Session`, `typeof auth.$Infer.Session.user`.
For separate client/server projects: `createAuthClient<typeof auth>()`.
---
## Common Gotchas
1. **Model vs table name** - Config uses ORM model name, not DB table name
2. **Plugin schema** - Re-run CLI after adding plugins
3. **Secondary storage** - Sessions go there by default, not DB
4. **Cookie cache** - Custom session fields NOT cached, always re-fetched
5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry
6. **Change email flow** - Sends to current email first, then new email
---
## Resources
- [Docs](https://better-auth.com/docs)
- [Options Reference](https://better-auth.com/docs/reference/options)
- [LLMs.txt](https://better-auth.com/llms.txt)
- [GitHub](https://github.com/better-auth/better-auth)
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/obra/superpowers/tree/main/skills/brainstorming",
"type": "github-subdir",
"installed_at": "2026-01-30T02:19:55.278306836Z",
"repo_url": "https://github.com/obra/superpowers.git",
"subdir": "skills/brainstorming",
"version": "469a6d8"
}

54
brainstorming/SKILL.md Normal file
View File

@@ -0,0 +1,54 @@
---
name: brainstorming
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
---
# Brainstorming Ideas Into Designs
## Overview
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far.
## The Process
**Understanding the idea:**
- Check out the current project state first (files, docs, recent commits)
- Ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria
**Exploring approaches:**
- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why
**Presenting the design:**
- Once you believe you understand what you're building, present the design
- Break it into sections of 200-300 words
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense
## After the Design
**Documentation:**
- Write the validated design to `docs/plans/YYYY-MM-DD-<topic>-design.md`
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git
**Implementation (if continuing):**
- Ask: "Ready to set up for implementation?"
- Use superpowers:using-git-worktrees to create isolated workspace
- Use superpowers:writing-plans to create detailed implementation plan
## Key Principles
- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design in sections, validate each
- **Be flexible** - Go back and clarify when something doesn't make sense

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/anthropics/skills/tree/main/skills/brand-guidelines",
"type": "github-subdir",
"installed_at": "2026-01-30T02:17:44.846548934Z",
"repo_url": "https://github.com/anthropics/skills.git",
"subdir": "skills/brand-guidelines",
"version": "69c0b1a"
}

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

73
brand-guidelines/SKILL.md Normal file
View File

@@ -0,0 +1,73 @@
---
name: brand-guidelines
description: Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
license: Complete terms in LICENSE.txt
---
# Anthropic Brand Styling
## Overview
To access Anthropic's official brand identity and style resources, use this skill.
**Keywords**: branding, corporate identity, visual identity, post-processing, styling, brand colors, typography, Anthropic brand, visual formatting, visual design
## Brand Guidelines
### Colors
**Main Colors:**
- Dark: `#141413` - Primary text and dark backgrounds
- Light: `#faf9f5` - Light backgrounds and text on dark
- Mid Gray: `#b0aea5` - Secondary elements
- Light Gray: `#e8e6dc` - Subtle backgrounds
**Accent Colors:**
- Orange: `#d97757` - Primary accent
- Blue: `#6a9bcc` - Secondary accent
- Green: `#788c5d` - Tertiary accent
### Typography
- **Headings**: Poppins (with Arial fallback)
- **Body Text**: Lora (with Georgia fallback)
- **Note**: Fonts should be pre-installed in your environment for best results
## Features
### Smart Font Application
- Applies Poppins font to headings (24pt and larger)
- Applies Lora font to body text
- Automatically falls back to Arial/Georgia if custom fonts unavailable
- Preserves readability across all systems
### Text Styling
- Headings (24pt+): Poppins font
- Body text: Lora font
- Smart color selection based on background
- Preserves text hierarchy and formatting
### Shape and Accent Colors
- Non-text shapes use accent colors
- Cycles through orange, blue, and green accents
- Maintains visual interest while staying on-brand
## Technical Details
### Font Management
- Uses system-installed Poppins and Lora fonts when available
- Provides automatic fallback to Arial (headings) and Georgia (body)
- No font installation required - works with existing system fonts
- For best results, pre-install Poppins and Lora fonts in your environment
### Color Application
- Uses RGB color values for precise brand matching
- Applied via python-pptx's RGBColor class
- Maintains color fidelity across different systems

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/cloudflare/skills/tree/main/skills/building-ai-agent-on-cloudflare",
"type": "github-subdir",
"installed_at": "2026-01-30T02:30:20.91375431Z",
"repo_url": "https://github.com/cloudflare/skills.git",
"subdir": "skills/building-ai-agent-on-cloudflare",
"version": "75a603b"
}

View File

@@ -0,0 +1,391 @@
---
name: building-ai-agent-on-cloudflare
description: |
Builds AI agents on Cloudflare using the Agents SDK with state management,
real-time WebSockets, scheduled tasks, tool integration, and chat capabilities.
Generates production-ready agent code deployed to Workers.
Use when: user wants to "build an agent", "AI agent", "chat agent", "stateful
agent", mentions "Agents SDK", needs "real-time AI", "WebSocket AI", or asks
about agent "state management", "scheduled tasks", or "tool calling".
---
# Building Cloudflare Agents
Creates AI-powered agents using Cloudflare's Agents SDK with persistent state, real-time communication, and tool integration.
## When to Use
- User wants to build an AI agent or chatbot
- User needs stateful, real-time AI interactions
- User asks about the Cloudflare Agents SDK
- User wants scheduled tasks or background AI work
- User needs WebSocket-based AI communication
## Prerequisites
- Cloudflare account with Workers enabled
- Node.js 18+ and npm/pnpm/yarn
- Wrangler CLI (`npm install -g wrangler`)
## Quick Start
```bash
npm create cloudflare@latest -- my-agent --template=cloudflare/agents-starter
cd my-agent
npm start
```
Agent runs at `http://localhost:8787`
## Core Concepts
### What is an Agent?
An Agent is a stateful, persistent AI service that:
- Maintains state across requests and reconnections
- Communicates via WebSockets or HTTP
- Runs on Cloudflare's edge via Durable Objects
- Can schedule tasks and call tools
- Scales horizontally (each user/session gets own instance)
### Agent Lifecycle
```
Client connects → Agent.onConnect() → Agent processes messages
→ Agent.onMessage()
→ Agent.setState() (persists + syncs)
Client disconnects → State persists → Client reconnects → State restored
```
## Basic Agent Structure
```typescript
import { Agent, Connection } from "agents";
interface Env {
AI: Ai; // Workers AI binding
}
interface State {
messages: Array<{ role: string; content: string }>;
preferences: Record<string, string>;
}
export class MyAgent extends Agent<Env, State> {
// Initial state for new instances
initialState: State = {
messages: [],
preferences: {},
};
// Called when agent starts or resumes
async onStart() {
console.log("Agent started with state:", this.state);
}
// Handle WebSocket connections
async onConnect(connection: Connection) {
connection.send(JSON.stringify({
type: "welcome",
history: this.state.messages,
}));
}
// Handle incoming messages
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
await this.handleChat(connection, data.content);
}
}
// Handle disconnections
async onClose(connection: Connection) {
console.log("Client disconnected");
}
// React to state changes
onStateUpdate(state: State, source: string) {
console.log("State updated by:", source);
}
private async handleChat(connection: Connection, userMessage: string) {
// Add user message to history
const messages = [
...this.state.messages,
{ role: "user", content: userMessage },
];
// Call AI
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages,
});
// Update state (persists and syncs to all clients)
this.setState({
...this.state,
messages: [
...messages,
{ role: "assistant", content: response.response },
],
});
// Send response
connection.send(JSON.stringify({
type: "response",
content: response.response,
}));
}
}
```
## Entry Point Configuration
```typescript
// src/index.ts
import { routeAgentRequest } from "agents";
import { MyAgent } from "./agent";
export default {
async fetch(request: Request, env: Env) {
// routeAgentRequest handles routing to /agents/:class/:name
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
};
export { MyAgent };
```
Clients connect via: `wss://my-agent.workers.dev/agents/MyAgent/session-id`
## Wrangler Configuration
```toml
name = "my-agent"
main = "src/index.ts"
compatibility_date = "2024-12-01"
[ai]
binding = "AI"
[durable_objects]
bindings = [{ name = "AGENT", class_name = "MyAgent" }]
[[migrations]]
tag = "v1"
new_classes = ["MyAgent"]
```
## State Management
### Reading State
```typescript
// Current state is always available
const currentMessages = this.state.messages;
const userPrefs = this.state.preferences;
```
### Updating State
```typescript
// setState persists AND syncs to all connected clients
this.setState({
...this.state,
messages: [...this.state.messages, newMessage],
});
// Partial updates work too
this.setState({
preferences: { ...this.state.preferences, theme: "dark" },
});
```
### SQL Storage
For complex queries, use the embedded SQLite database:
```typescript
// Create tables
await this.sql`
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`;
// Insert
await this.sql`
INSERT INTO documents (title, content)
VALUES (${title}, ${content})
`;
// Query
const docs = await this.sql`
SELECT * FROM documents WHERE title LIKE ${`%${search}%`}
`;
```
## Scheduled Tasks
Agents can schedule future work:
```typescript
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "schedule_reminder") {
// Schedule task for 1 hour from now
const { id } = await this.schedule(3600, "sendReminder", {
message: data.reminderText,
userId: data.userId,
});
connection.send(JSON.stringify({ type: "scheduled", taskId: id }));
}
}
// Called when scheduled task fires
async sendReminder(data: { message: string; userId: string }) {
// Send notification, email, etc.
console.log(`Reminder for ${data.userId}: ${data.message}`);
// Can also update state
this.setState({
...this.state,
lastReminder: new Date().toISOString(),
});
}
```
### Schedule Options
```typescript
// Delay in seconds
await this.schedule(60, "taskMethod", { data });
// Specific date
await this.schedule(new Date("2025-01-01T00:00:00Z"), "taskMethod", { data });
// Cron expression (recurring)
await this.schedule("0 9 * * *", "dailyTask", {}); // 9 AM daily
await this.schedule("*/5 * * * *", "everyFiveMinutes", {}); // Every 5 min
// Manage schedules
const schedules = await this.getSchedules();
await this.cancelSchedule(taskId);
```
## Chat Agent (AI-Powered)
For chat-focused agents, extend `AIChatAgent`:
```typescript
import { AIChatAgent } from "agents/ai-chat-agent";
export class ChatBot extends AIChatAgent<Env> {
// Called for each user message
async onChatMessage(message: string) {
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{ role: "system", content: "You are a helpful assistant." },
...this.messages, // Automatic history management
{ role: "user", content: message },
],
stream: true,
});
// Stream response back to client
return response;
}
}
```
Features included:
- Automatic message history
- Resumable streaming (survives disconnects)
- Built-in `saveMessages()` for persistence
## Client Integration
### React Hook
```tsx
import { useAgent } from "agents/react";
function Chat() {
const { state, send, connected } = useAgent({
agent: "my-agent",
name: userId, // Agent instance ID
});
const sendMessage = (text: string) => {
send(JSON.stringify({ type: "chat", content: text }));
};
return (
<div>
{state.messages.map((msg, i) => (
<div key={i}>{msg.role}: {msg.content}</div>
))}
<input onKeyDown={(e) => e.key === "Enter" && sendMessage(e.target.value)} />
</div>
);
}
```
### Vanilla JavaScript
```javascript
const ws = new WebSocket("wss://my-agent.workers.dev/agents/MyAgent/user123");
ws.onopen = () => {
console.log("Connected to agent");
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Received:", data);
};
ws.send(JSON.stringify({ type: "chat", content: "Hello!" }));
```
## Common Patterns
See [references/agent-patterns.md](references/agent-patterns.md) for:
- Tool calling and function execution
- Multi-agent orchestration
- RAG (Retrieval Augmented Generation)
- Human-in-the-loop workflows
## Deployment
```bash
# Deploy
npx wrangler deploy
# View logs
wrangler tail
# Test endpoint
curl https://my-agent.workers.dev/agents/MyAgent/test-user
```
## Troubleshooting
See [references/troubleshooting.md](references/troubleshooting.md) for common issues.
## References
- [references/examples.md](references/examples.md) — Official templates and production examples
- [references/agent-patterns.md](references/agent-patterns.md) — Advanced patterns
- [references/state-patterns.md](references/state-patterns.md) — State management strategies
- [references/troubleshooting.md](references/troubleshooting.md) — Error solutions

View File

@@ -0,0 +1,461 @@
# Agent Patterns
Advanced patterns for building sophisticated agents.
## Tool Calling
Agents can expose tools that AI models can call:
```typescript
import { Agent, Connection } from "agents";
import { z } from "zod";
interface Tool {
name: string;
description: string;
parameters: z.ZodSchema;
handler: (params: any) => Promise<string>;
}
export class ToolAgent extends Agent<Env, State> {
private tools: Map<string, Tool> = new Map();
async onStart() {
// Register tools
this.registerTool({
name: "get_weather",
description: "Get current weather for a city",
parameters: z.object({ city: z.string() }),
handler: async ({ city }) => {
const res = await fetch(`https://api.weather.com/${city}`);
return JSON.stringify(await res.json());
},
});
this.registerTool({
name: "search_database",
description: "Search the document database",
parameters: z.object({ query: z.string(), limit: z.number().default(10) }),
handler: async ({ query, limit }) => {
const results = await this.sql`
SELECT * FROM documents
WHERE content LIKE ${`%${query}%`}
LIMIT ${limit}
`;
return JSON.stringify(results);
},
});
}
private registerTool(tool: Tool) {
this.tools.set(tool.name, tool);
}
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
await this.handleChatWithTools(connection, data.content);
}
}
private async handleChatWithTools(connection: Connection, userMessage: string) {
// Build tool descriptions for the AI
const toolDescriptions = Array.from(this.tools.values()).map((t) => ({
type: "function",
function: {
name: t.name,
description: t.description,
parameters: JSON.parse(JSON.stringify(t.parameters)),
},
}));
// First AI call - may request tool use
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{ role: "system", content: "You are a helpful assistant with access to tools." },
...this.state.messages,
{ role: "user", content: userMessage },
],
tools: toolDescriptions,
});
// Check if AI wants to use a tool
if (response.tool_calls) {
for (const toolCall of response.tool_calls) {
const tool = this.tools.get(toolCall.function.name);
if (tool) {
const params = JSON.parse(toolCall.function.arguments);
const result = await tool.handler(params);
// Send tool result back to AI
const finalResponse = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
...this.state.messages,
{ role: "user", content: userMessage },
{ role: "assistant", tool_calls: response.tool_calls },
{ role: "tool", tool_call_id: toolCall.id, content: result },
],
});
connection.send(JSON.stringify({
type: "response",
content: finalResponse.response,
toolUsed: toolCall.function.name,
}));
}
}
} else {
connection.send(JSON.stringify({
type: "response",
content: response.response,
}));
}
}
}
```
## RAG (Retrieval Augmented Generation)
Combine Vectorize with Agents for knowledge-grounded responses:
```typescript
interface Env {
AI: Ai;
VECTORIZE: VectorizeIndex;
}
export class RAGAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
// 1. Generate embedding for query
const embedding = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: data.content,
});
// 2. Search vector database
const results = await this.env.VECTORIZE.query(embedding.data[0], {
topK: 5,
returnMetadata: true,
});
// 3. Build context from results
const context = results.matches
.map((m) => m.metadata?.text || "")
.join("\n\n");
// 4. Generate response with context
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{
role: "system",
content: `Answer based on this context:\n\n${context}\n\nIf the context doesn't contain relevant information, say so.`,
},
{ role: "user", content: data.content },
],
});
// 5. Update state and respond
this.setState({
messages: [
...this.state.messages,
{ role: "user", content: data.content },
{ role: "assistant", content: response.response },
],
});
connection.send(JSON.stringify({
type: "response",
content: response.response,
sources: results.matches.map((m) => m.metadata?.source),
}));
}
}
// Ingest documents into vector store
async ingestDocument(doc: { id: string; text: string; source: string }) {
const embedding = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: doc.text,
});
await this.env.VECTORIZE.upsert([{
id: doc.id,
values: embedding.data[0],
metadata: { text: doc.text, source: doc.source },
}]);
}
}
```
## Multi-Agent Orchestration
Coordinate multiple specialized agents:
```typescript
interface Env {
RESEARCHER: DurableObjectNamespace;
WRITER: DurableObjectNamespace;
REVIEWER: DurableObjectNamespace;
}
export class OrchestratorAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "create_article") {
connection.send(JSON.stringify({ type: "status", step: "researching" }));
// Step 1: Research agent gathers information
const researchResult = await this.callAgent(
this.env.RESEARCHER,
data.topic,
{ action: "research", topic: data.topic }
);
connection.send(JSON.stringify({ type: "status", step: "writing" }));
// Step 2: Writer agent creates draft
const draftResult = await this.callAgent(
this.env.WRITER,
data.topic,
{ action: "write", research: researchResult, topic: data.topic }
);
connection.send(JSON.stringify({ type: "status", step: "reviewing" }));
// Step 3: Reviewer agent improves draft
const finalResult = await this.callAgent(
this.env.REVIEWER,
data.topic,
{ action: "review", draft: draftResult }
);
connection.send(JSON.stringify({
type: "complete",
article: finalResult,
}));
}
}
private async callAgent(
namespace: DurableObjectNamespace,
id: string,
payload: any
): Promise<string> {
const agentId = namespace.idFromName(id);
const agent = namespace.get(agentId);
const response = await agent.fetch("http://agent/task", {
method: "POST",
body: JSON.stringify(payload),
});
return response.text();
}
}
```
## Human-in-the-Loop
Pause agent execution for human approval:
```typescript
interface State {
pendingApprovals: Array<{
id: string;
action: string;
data: any;
requestedAt: string;
}>;
}
export class ApprovalAgent extends Agent<Env, State> {
initialState: State = { pendingApprovals: [] };
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "request_action") {
// Action requires approval
if (this.requiresApproval(data.action)) {
const approvalId = crypto.randomUUID();
this.setState({
pendingApprovals: [
...this.state.pendingApprovals,
{
id: approvalId,
action: data.action,
data: data.payload,
requestedAt: new Date().toISOString(),
},
],
});
connection.send(JSON.stringify({
type: "approval_required",
approvalId,
action: data.action,
description: this.describeAction(data.action, data.payload),
}));
return;
}
// Execute immediately if no approval needed
await this.executeAction(connection, data.action, data.payload);
}
if (data.type === "approve") {
const approval = this.state.pendingApprovals.find(
(a) => a.id === data.approvalId
);
if (approval) {
// Remove from pending
this.setState({
pendingApprovals: this.state.pendingApprovals.filter(
(a) => a.id !== data.approvalId
),
});
// Execute the approved action
await this.executeAction(connection, approval.action, approval.data);
}
}
if (data.type === "reject") {
this.setState({
pendingApprovals: this.state.pendingApprovals.filter(
(a) => a.id !== data.approvalId
),
});
connection.send(JSON.stringify({
type: "action_rejected",
approvalId: data.approvalId,
}));
}
}
private requiresApproval(action: string): boolean {
const sensitiveActions = ["delete", "send_email", "make_payment", "publish"];
return sensitiveActions.includes(action);
}
private describeAction(action: string, data: any): string {
// Generate human-readable description
return `${action}: ${JSON.stringify(data)}`;
}
private async executeAction(connection: Connection, action: string, data: any) {
// Execute the action
const result = await this.performAction(action, data);
connection.send(JSON.stringify({
type: "action_completed",
action,
result,
}));
}
}
```
## Streaming Responses
Stream AI responses in real-time:
```typescript
export class StreamingAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
// Start streaming response
const stream = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{ role: "system", content: "You are a helpful assistant." },
...this.state.messages,
{ role: "user", content: data.content },
],
stream: true,
});
let fullResponse = "";
// Stream chunks to client
for await (const chunk of stream) {
if (chunk.response) {
fullResponse += chunk.response;
connection.send(JSON.stringify({
type: "stream",
content: chunk.response,
done: false,
}));
}
}
// Update state with complete response
this.setState({
messages: [
...this.state.messages,
{ role: "user", content: data.content },
{ role: "assistant", content: fullResponse },
],
});
// Signal completion
connection.send(JSON.stringify({
type: "stream",
content: "",
done: true,
}));
}
}
}
```
## Connecting to MCP Servers
Agents can connect to MCP servers as clients:
```typescript
export class MCPClientAgent extends Agent<Env, State> {
async onStart() {
// Connect to external MCP server
await this.addMcpServer(
"github",
"https://github-mcp.example.com/sse",
{ headers: { Authorization: `Bearer ${this.env.GITHUB_TOKEN}` } }
);
await this.addMcpServer(
"database",
"https://db-mcp.example.com/sse"
);
}
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "use_tool") {
// Call tool on connected MCP server
const servers = await this.getMcpServers();
const server = servers.find((s) => s.name === data.server);
if (server) {
const result = await server.callTool(data.tool, data.params);
connection.send(JSON.stringify({ type: "tool_result", result }));
}
}
}
async onClose() {
// Cleanup MCP connections
await this.removeMcpServer("github");
await this.removeMcpServer("database");
}
}
```

View File

@@ -0,0 +1,188 @@
# Project Bootstrapping
Instructions for creating new agent projects.
---
## Create Command
Execute in terminal to generate a new project:
```bash
npm create cloudflare@latest -- my-agent \
--template=cloudflare/agents-starter
```
Or use npx directly:
```bash
npx create-cloudflare@latest --template cloudflare/agents-starter
```
Includes:
- Persistent data via `this.setState` and `this.sql`
- WebSocket real-time connections
- Workers AI bindings ready
- React chat interface example
---
## Project Layout
Generated structure:
```
my-agent/
├── src/
│ ├── app.tsx # React chat interface
│ ├── server.ts # Agent implementation
│ ├── tools.ts # Tool definitions
│ └── utils.ts # Helpers
├── wrangler.toml # Platform configuration
└── package.json
```
---
## Agent Variations
**Chat-focused:**
Inherit from base `Agent` class, implement `onMessage` handler:
- Manual conversation tracking
- Full control over responses
- Integrates with any AI provider
**Persistent data:**
Use `this.setState()` for automatic persistence:
- JSON-serializable data
- Auto-syncs to connected clients
- Survives instance eviction
**Per-session isolation:**
Route by unique identifier in URL path:
- Each identifier gets dedicated instance
- Isolated data storage
- Horizontal scaling automatic
---
## Platform Documentation
- developers.cloudflare.com/agents/
- developers.cloudflare.com/agents/getting-started/
- developers.cloudflare.com/agents/api-reference/
**Source repositories:**
- `github.com/cloudflare/agents-starter` (starter template)
- `github.com/cloudflare/agents/tree/main/examples` (reference implementations)
**Related services:**
- developers.cloudflare.com/workers-ai/ (AI models)
- developers.cloudflare.com/vectorize/ (vector search)
- developers.cloudflare.com/d1/ (SQL database)
---
## Reference Implementations
Located at `github.com/cloudflare/agents/tree/main/examples`:
| Example | Description |
|---------|-------------|
| `resumable-stream-chat` | Chat with reconnection-safe streaming |
| `email-agent` | Handle incoming emails via Email Routing |
| `mcp-client` | Connect agents to external MCP servers |
| `mcp-worker` | Expose agent capabilities via MCP protocol |
| `cross-domain` | Multi-domain authentication patterns |
| `tictactoe` | Multiplayer game with shared state |
| `a2a` | Agent-to-agent communication |
| `codemode` | Code transformation workflows |
| `playground` | Interactive testing sandbox |
Browse each folder for complete implementation code and wrangler configuration.
---
## Selection Matrix
| Goal | Approach |
|------|----------|
| Conversational bot | Agent + onMessage handler |
| Custom data schema | Agent + setState() |
| Knowledge retrieval | Agent + Vectorize |
| Background jobs | Agent + schedule() |
| External integrations | Agent + tool definitions |
---
## Commands Reference
**Local execution:**
```bash
cd my-agent
npm install
npm start
# Accessible at http://localhost:8787
```
**Production push:**
```bash
npx wrangler deploy
# Accessible at https://[name].[subdomain].workers.dev
```
**WebSocket connection:**
```javascript
// URL pattern: /agents/:className/:instanceName
const socket = new WebSocket("wss://my-agent.workers.dev/agents/MyAgent/session-123");
socket.onmessage = (e) => {
console.log("Received:", JSON.parse(e.data));
};
socket.send(JSON.stringify({ type: "chat", content: "Hello" }));
```
**React integration:**
```tsx
import { useAgent } from "agents/react";
function Chat() {
const { state, send } = useAgent({
agent: "my-agent",
name: "session-123",
});
// state auto-updates, send() dispatches messages
}
```
---
## Key Methods (from Agent class)
| Method | Purpose |
|--------|---------|
| `onStart()` | Runs on instance startup |
| `onConnect()` | Handles new WebSocket connections |
| `onMessage()` | Processes incoming messages |
| `onClose()` | Cleanup on disconnect |
| `setState()` | Persist and broadcast data |
| `this.sql` | Query embedded SQLite |
| `schedule()` | Delayed/recurring tasks |
| `broadcast()` | Message all connections |
---
## Help Channels
- Cloudflare Discord
- GitHub discussions on cloudflare/agents repository

View File

@@ -0,0 +1,360 @@
# State Management Patterns
Strategies for managing state in Cloudflare Agents.
## How State Works
State is automatically persisted to the `cf_agents_state` SQL table. The `this.state` getter lazily loads from storage, while `this.setState()` serializes and persists changes. State survives Durable Object evictions.
```typescript
class MyAgent extends Agent<Env, { count: number }> {
initialState = { count: 0 };
increment() {
this.setState({ count: this.state.count + 1 });
}
onStateUpdate(state: State, source: string) {
console.log("State updated by:", source);
}
}
```
## State vs SQL: When to Use Which
### Use `this.state` + `setState()` When:
- Data is small (< 1MB recommended)
- Needs real-time sync to all connected clients
- Simple key-value or object structure
- Frequently read, occasionally updated
```typescript
interface State {
currentUser: { id: string; name: string };
preferences: Record<string, string>;
recentMessages: Message[]; // Keep limited, e.g., last 50
isTyping: boolean;
}
```
### Use `this.sql` When:
- Large datasets (many records)
- Complex queries (JOINs, aggregations, filtering)
- Historical data / audit logs
- Data that doesn't need real-time sync
```typescript
// Good for SQL
// - Full message history
// - User documents
// - Analytics events
// - Search indexes
```
## Hybrid Pattern
Combine both for optimal performance:
```typescript
interface State {
recentMessages: Message[];
onlineUsers: string[];
currentDocument: Document | null;
}
export class HybridAgent extends Agent<Env, State> {
initialState: State = {
recentMessages: [],
onlineUsers: [],
currentDocument: null,
};
async onStart() {
await this.sql`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`;
const recent = await this.sql`
SELECT * FROM messages
ORDER BY created_at DESC
LIMIT 50
`;
this.setState({
...this.state,
recentMessages: recent.reverse(),
});
}
async addMessage(message: Message) {
await this.sql`
INSERT INTO messages (id, user_id, content)
VALUES (${message.id}, ${message.userId}, ${message.content})
`;
const recentMessages = [...this.state.recentMessages, message].slice(-50);
this.setState({ ...this.state, recentMessages });
}
}
```
---
## Queue System
The SDK includes a built-in queue for background task processing. Tasks are stored in SQLite and processed in FIFO order.
### Queue Methods
| Method | Purpose |
|--------|---------|
| `queue(callback, payload)` | Add task, returns task ID |
| `dequeue(id)` | Remove specific task |
| `dequeueAll()` | Clear entire queue |
| `dequeueAllByCallback(name)` | Remove tasks by callback name |
| `getQueue(id)` | Get single task |
| `getQueues(key, value)` | Find tasks by payload field |
### Queue Example
```typescript
export class TaskAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "process_later") {
const taskId = await this.queue("processItem", {
itemId: data.itemId,
priority: data.priority,
});
connection.send(JSON.stringify({ queued: true, taskId }));
}
}
// Callback receives payload and QueueItem metadata
async processItem(payload: { itemId: string }, item: QueueItem) {
console.log(`Processing ${payload.itemId}, queued at ${item.createdAt}`);
// Successfully executed tasks are auto-removed
}
}
```
**Queue characteristics:**
- Sequential processing (no parallelization)
- Persists across agent restarts
- No built-in retry mechanism
- Payloads must be JSON-serializable
---
## Context Management
Custom methods automatically have full agent context. Use `getCurrentAgent()` to access context from external functions.
```typescript
import { getCurrentAgent } from "agents";
// External utility function
async function logActivity(action: string) {
const { agent } = getCurrentAgent<MyAgent>();
await agent.sql`
INSERT INTO activity_log (action, timestamp)
VALUES (${action}, ${Date.now()})
`;
}
export class MyAgent extends Agent<Env, State> {
async performAction() {
// Context automatically available
await logActivity("action_performed");
}
}
```
`getCurrentAgent<T>()` returns:
- `agent` - The current agent instance
- `connection` - Connection object (if applicable)
- `request` - Request object (if applicable)
---
## State Synchronization
### Optimistic Updates
Update UI immediately, then persist:
```typescript
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "update_preference") {
this.setState({
...this.state,
preferences: {
...this.state.preferences,
[data.key]: data.value,
},
});
await this.sql`
INSERT OR REPLACE INTO preferences (key, value)
VALUES (${data.key}, ${data.value})
`;
}
}
```
### Conflict Resolution
Handle concurrent updates with versioning:
```typescript
interface State {
document: {
content: string;
version: number;
lastModifiedBy: string;
};
}
async updateDocument(userId: string, newContent: string, expectedVersion: number) {
if (this.state.document.version !== expectedVersion) {
throw new Error("Conflict: document was modified by another user");
}
this.setState({
...this.state,
document: {
content: newContent,
version: expectedVersion + 1,
lastModifiedBy: userId,
},
});
}
```
### Per-Connection State
Track ephemeral state for each connected client:
```typescript
export class MultiUserAgent extends Agent<Env, State> {
private connectionState = new Map<string, {
userId: string;
cursor: { x: number; y: number };
lastActivity: number;
}>();
async onConnect(connection: Connection) {
this.connectionState.set(connection.id, {
userId: "",
cursor: { x: 0, y: 0 },
lastActivity: Date.now(),
});
}
async onClose(connection: Connection) {
this.connectionState.delete(connection.id);
}
}
```
---
## State Migration
When state schema changes:
```typescript
interface StateV2 {
messages: Array<{ id: string; content: string; timestamp: string }>;
version: 2;
}
export class MigratingAgent extends Agent<Env, StateV2> {
initialState: StateV2 = {
messages: [],
version: 2,
};
async onStart() {
const rawState = this.state as any;
if (!rawState.version || rawState.version < 2) {
const migratedMessages = (rawState.messages || []).map(
(content: string, i: number) => ({
id: `migrated-${i}`,
content,
timestamp: new Date().toISOString(),
})
);
this.setState({
messages: migratedMessages,
version: 2,
});
}
}
}
```
---
## State Size Management
Keep state lean for performance:
```typescript
export class LeanStateAgent extends Agent<Env, State> {
private readonly MAX_RECENT_MESSAGES = 100;
async addMessage(message: Message) {
await this.sql`INSERT INTO messages (id, content) VALUES (${message.id}, ${message.content})`;
let recentMessages = [...this.state.recentMessages, message];
if (recentMessages.length > this.MAX_RECENT_MESSAGES) {
recentMessages = recentMessages.slice(-this.MAX_RECENT_MESSAGES);
}
this.setState({
...this.state,
recentMessages,
stats: {
...this.state.stats,
totalMessages: this.state.stats.totalMessages + 1,
lastActivity: new Date().toISOString(),
},
});
}
}
```
---
## Debugging State
```typescript
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "debug_state") {
connection.send(JSON.stringify({
type: "debug_response",
state: this.state,
stateSize: JSON.stringify(this.state).length,
sqlTables: await this.sql`
SELECT name FROM sqlite_master WHERE type='table'
`,
}));
}
}
```

View File

@@ -0,0 +1,362 @@
# Agent Troubleshooting
Common issues and solutions for Cloudflare Agents.
## Connection Issues
### "WebSocket connection failed"
**Symptoms:** Client cannot connect to agent.
**Causes & Solutions:**
1. **Worker not deployed**
```bash
wrangler deployments list
wrangler deploy # If not deployed
```
2. **Wrong URL path**
```javascript
// Ensure your routing handles the agent path
// Client:
new WebSocket("wss://my-worker.workers.dev/agent/user123");
// Worker must route to agent:
if (url.pathname.startsWith("/agent/")) {
const id = url.pathname.split("/")[2];
return env.AGENT.get(env.AGENT.idFromName(id)).fetch(request);
}
```
3. **CORS issues (browser clients)**
Agents handle WebSocket upgrades automatically, but ensure your entry worker doesn't block the request.
### "Connection closed unexpectedly"
1. **Agent threw an error**
```bash
wrangler tail # Check for exceptions
```
2. **Message handler crashed**
```typescript
async onMessage(connection: Connection, message: string) {
try {
// Your logic
} catch (error) {
console.error("Message handling error:", error);
connection.send(JSON.stringify({ type: "error", message: error.message }));
}
}
```
3. **Hibernation woke agent with stale connection**
Ensure you handle reconnection gracefully in client code.
## State Issues
### "State not persisting"
**Causes:**
1. **Didn't call `setState()`**
```typescript
// Wrong - direct mutation doesn't persist
this.state.messages.push(newMessage);
// Correct - use setState
this.setState({
...this.state,
messages: [...this.state.messages, newMessage],
});
```
2. **Agent crashed before state saved**
`setState()` is durable, but if agent crashes during processing before `setState()`, changes are lost.
3. **Wrong agent instance**
Each unique ID gets a separate agent. Ensure clients connect to the same ID.
### "State out of sync between clients"
`setState()` automatically syncs to all connected clients via `onStateUpdate()`. If sync isn't working:
1. **Check `onStateUpdate` is implemented**
```typescript
onStateUpdate(state: State, source: string) {
// This fires when state changes from any source
console.log("State updated:", state, "from:", source);
}
```
2. **Client not listening for state updates**
```typescript
// React hook handles this automatically
const { state } = useAgent({ agent: "my-agent", name: id });
// Manual WebSocket - listen for state messages
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "state_update") {
updateLocalState(data.state);
}
};
```
### "State too large" / Performance issues
State is serialized as JSON. Keep it small:
```typescript
// Bad - storing everything in state
interface State {
allMessages: Message[]; // Could be thousands
allDocuments: Document[];
}
// Good - state for hot data, SQL for cold
interface State {
recentMessages: Message[]; // Last 50 only
currentDocument: Document | null;
}
// Store full history in SQL
await this.sql`INSERT INTO messages ...`;
```
## SQL Issues
### "no such table"
Table not created. Create in `onStart()`:
```typescript
async onStart() {
await this.sql`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`;
}
```
### "SQL logic error"
Check your query syntax. Use tagged templates correctly:
```typescript
// Wrong - string interpolation (SQL injection risk!)
await this.sql`SELECT * FROM users WHERE id = '${userId}'`;
// Correct - parameterized query
await this.sql`SELECT * FROM users WHERE id = ${userId}`;
```
### SQL query returns empty
1. **Wrong table name**
2. **Data in different agent instance** (each agent ID has isolated storage)
3. **Query conditions don't match**
Debug:
```typescript
const tables = await this.sql`
SELECT name FROM sqlite_master WHERE type='table'
`;
console.log("Tables:", tables);
const count = await this.sql`SELECT COUNT(*) as count FROM messages`;
console.log("Message count:", count);
```
## Scheduled Task Issues
### "Task never fires"
1. **Method name mismatch**
```typescript
// Schedule references method that must exist
await this.schedule(60, "sendReminder", { ... });
// Method must be defined on the class
async sendReminder(data: any) {
// This method MUST exist
}
```
2. **Cron syntax error**
```typescript
// Invalid cron
await this.schedule("every 5 minutes", "task", {}); // Wrong
// Valid cron
await this.schedule("*/5 * * * *", "task", {}); // Every 5 minutes
```
3. **Task was cancelled**
```typescript
const schedules = await this.getSchedules();
console.log("Active schedules:", schedules);
```
### "Task fires multiple times"
If you schedule in `onStart()` without checking:
```typescript
async onStart() {
// Bad - schedules new task every time agent wakes
await this.schedule("0 9 * * *", "dailyTask", {});
// Good - check first
const schedules = await this.getSchedules();
const hasDaily = schedules.some(s => s.callback === "dailyTask");
if (!hasDaily) {
await this.schedule("0 9 * * *", "dailyTask", {});
}
}
```
## Deployment Issues
### "Class MyAgent is not exported"
```typescript
// src/index.ts - Must export the class
export { MyAgent } from "./agent";
// Or if defined in same file
export class MyAgent extends Agent { ... }
```
### "Durable Object not found"
Check `wrangler.toml`:
```toml
[durable_objects]
bindings = [{ name = "AGENT", class_name = "MyAgent" }]
[[migrations]]
tag = "v1"
new_classes = ["MyAgent"]
```
### "Migration required"
When adding new Durable Object classes:
```toml
[[migrations]]
tag = "v2" # Increment from previous
new_classes = ["NewAgentClass"]
# Or for renames
# renamed_classes = [{ from = "OldName", to = "NewName" }]
```
## AI Integration Issues
### "AI binding not found"
Add to `wrangler.toml`:
```toml
[ai]
binding = "AI"
```
### "Model not found" / "Rate limited"
```typescript
// Check model name is correct
const response = await this.env.AI.run(
"@cf/meta/llama-3-8b-instruct", // Exact model name
{ messages: [...] }
);
// Handle rate limits
try {
const response = await this.env.AI.run(...);
} catch (error) {
if (error.message.includes("rate limit")) {
// Retry with backoff or use queue
}
}
```
### "Streaming not working"
```typescript
// Enable streaming
const stream = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [...],
stream: true, // Must be true
});
// Iterate over stream
for await (const chunk of stream) {
connection.send(JSON.stringify({ type: "chunk", content: chunk.response }));
}
```
## Debugging Tips
### Enable Verbose Logging
```typescript
export class MyAgent extends Agent<Env, State> {
async onStart() {
console.log("Agent starting, state:", JSON.stringify(this.state));
}
async onConnect(connection: Connection) {
console.log("Client connected:", connection.id);
}
async onMessage(connection: Connection, message: string) {
console.log("Received message:", message);
// ... handle
console.log("State after:", JSON.stringify(this.state));
}
async onClose(connection: Connection) {
console.log("Client disconnected:", connection.id);
}
}
```
View logs:
```bash
wrangler tail --format pretty
```
### Test Locally First
```bash
npm start
# Connect with test client or use browser console:
# new WebSocket("ws://localhost:8787/agent/test")
```
### Inspect State
Add a debug endpoint:
```typescript
async onRequest(request: Request) {
const url = new URL(request.url);
if (url.pathname === "/debug") {
return Response.json({
state: this.state,
schedules: await this.getSchedules(),
});
}
return new Response("Not found", { status: 404 });
}
```

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/cloudflare/skills/tree/main/skills/building-mcp-server-on-cloudflare",
"type": "github-subdir",
"installed_at": "2026-01-30T02:30:25.030942117Z",
"repo_url": "https://github.com/cloudflare/skills.git",
"subdir": "skills/building-mcp-server-on-cloudflare",
"version": "75a603b"
}

View File

@@ -0,0 +1,265 @@
---
name: building-mcp-server-on-cloudflare
description: |
Builds remote MCP (Model Context Protocol) servers on Cloudflare Workers
with tools, OAuth authentication, and production deployment. Generates
server code, configures auth providers, and deploys to Workers.
Use when: user wants to "build MCP server", "create MCP tools", "remote
MCP", "deploy MCP", add "OAuth to MCP", or mentions Model Context Protocol
on Cloudflare. Also triggers on "MCP authentication" or "MCP deployment".
---
# Building MCP Servers on Cloudflare
Creates production-ready Model Context Protocol servers on Cloudflare Workers with tools, authentication, and deployment.
## When to Use
- User wants to build a remote MCP server
- User needs to expose tools via MCP
- User asks about MCP authentication or OAuth
- User wants to deploy MCP to Cloudflare Workers
## Prerequisites
- Cloudflare account with Workers enabled
- Node.js 18+ and npm/pnpm/yarn
- Wrangler CLI (`npm install -g wrangler`)
## Quick Start
### Option 1: Public Server (No Auth)
```bash
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-authless
cd my-mcp-server
npm start
```
Server runs at `http://localhost:8788/mcp`
### Option 2: Authenticated Server (OAuth)
```bash
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-github-oauth
cd my-mcp-server
```
Requires OAuth app setup. See [references/oauth-setup.md](references/oauth-setup.md).
## Core Workflow
### Step 1: Define Tools
Tools are functions MCP clients can call. Define them using `server.tool()`:
```typescript
import { McpAgent } from "agents/mcp";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new Server({ name: "my-mcp", version: "1.0.0" });
async init() {
// Simple tool with parameters
this.server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
// Tool that calls external API
this.server.tool(
"get_weather",
{ city: z.string() },
async ({ city }) => {
const response = await fetch(`https://api.weather.com/${city}`);
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data) }],
};
}
);
}
}
```
### Step 2: Configure Entry Point
**Public server** (`src/index.ts`):
```typescript
import { MyMCP } from "./mcp";
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
if (url.pathname === "/mcp") {
return MyMCP.serveSSE("/mcp").fetch(request, env, ctx);
}
return new Response("MCP Server", { status: 200 });
},
};
export { MyMCP };
```
**Authenticated server** — See [references/oauth-setup.md](references/oauth-setup.md).
### Step 3: Test Locally
```bash
# Start server
npm start
# In another terminal, test with MCP Inspector
npx @modelcontextprotocol/inspector@latest
# Open http://localhost:5173, enter http://localhost:8788/mcp
```
### Step 4: Deploy
```bash
npx wrangler deploy
```
Server accessible at `https://[worker-name].[account].workers.dev/mcp`
### Step 5: Connect Clients
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-remote", "https://my-mcp.workers.dev/mcp"]
}
}
}
```
Restart Claude Desktop after updating config.
## Tool Patterns
### Return Types
```typescript
// Text response
return { content: [{ type: "text", text: "result" }] };
// Multiple content items
return {
content: [
{ type: "text", text: "Here's the data:" },
{ type: "text", text: JSON.stringify(data, null, 2) },
],
};
```
### Input Validation with Zod
```typescript
this.server.tool(
"create_user",
{
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "user", "guest"]),
age: z.number().int().min(0).optional(),
},
async (params) => {
// params are fully typed and validated
}
);
```
### Accessing Environment/Bindings
```typescript
export class MyMCP extends McpAgent<Env> {
async init() {
this.server.tool("query_db", { sql: z.string() }, async ({ sql }) => {
// Access D1 binding
const result = await this.env.DB.prepare(sql).all();
return { content: [{ type: "text", text: JSON.stringify(result) }] };
});
}
}
```
## Authentication
For OAuth-protected servers, see [references/oauth-setup.md](references/oauth-setup.md).
Supported providers:
- GitHub
- Google
- Auth0
- Stytch
- WorkOS
- Any OAuth 2.0 compliant provider
## Wrangler Configuration
Minimal `wrangler.toml`:
```toml
name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-12-01"
[durable_objects]
bindings = [{ name = "MCP", class_name = "MyMCP" }]
[[migrations]]
tag = "v1"
new_classes = ["MyMCP"]
```
With bindings (D1, KV, etc.):
```toml
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxx"
[[kv_namespaces]]
binding = "KV"
id = "xxx"
```
## Common Issues
### "Tool not found" in Client
1. Verify tool name matches exactly (case-sensitive)
2. Ensure `init()` registers tools before connections
3. Check server logs: `wrangler tail`
### Connection Fails
1. Confirm endpoint path is `/mcp`
2. Check CORS if browser-based client
3. Verify Worker is deployed: `wrangler deployments list`
### OAuth Redirect Errors
1. Callback URL must match OAuth app config exactly
2. Check `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` are set
3. For local dev, use `http://localhost:8788/callback`
## References
- [references/examples.md](references/examples.md) — Official templates and production examples
- [references/oauth-setup.md](references/oauth-setup.md) — OAuth provider configuration
- [references/tool-patterns.md](references/tool-patterns.md) — Advanced tool examples
- [references/troubleshooting.md](references/troubleshooting.md) — Error codes and fixes

View File

@@ -0,0 +1,115 @@
# Project Bootstrapping
Instructions for creating new MCP server projects.
---
## Create Commands
Execute in terminal to generate a new project:
**Without authentication:**
```bash
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-authless
```
**With GitHub login:**
```bash
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-github-oauth
```
**With Google login:**
```bash
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-google-oauth
```
---
## Additional Boilerplate Locations
**Main repository:** `github.com/cloudflare/ai` (check demos directory)
Other authentication providers:
- Auth0
- WorkOS AuthKit
- Logto
- Descope
- Stytch
**Cloudflare tooling:** `github.com/cloudflare/mcp-server-cloudflare`
---
## Selection Matrix
| Goal | Boilerplate |
|------|-------------|
| Testing/learning | authless |
| GitHub API access | github-oauth |
| Google API access | google-oauth |
| Enterprise auth | auth0 / authkit |
| Slack apps | slack-oauth |
| Zero Trust | cf-access |
---
## Platform Documentation
- developers.cloudflare.com/agents/model-context-protocol/
- developers.cloudflare.com/agents/guides/remote-mcp-server/
- developers.cloudflare.com/agents/guides/test-remote-mcp-server/
- developers.cloudflare.com/agents/model-context-protocol/authorization/
---
## Commands Reference
**Local execution:**
```bash
cd my-mcp-server
npm install
npm start
# Accessible at http://localhost:8788/mcp
```
**Production push:**
```bash
npx wrangler deploy
# Accessible at https://[worker-name].[subdomain].workers.dev/mcp
```
**Claude Desktop setup** (modify `claude_desktop_config.json`):
```json
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-remote", "https://my-mcp-server.my-account.workers.dev/mcp"]
}
}
}
```
**Inspector testing:**
```bash
npx @modelcontextprotocol/inspector@latest
# Launch browser at http://localhost:5173
# Input your server URL: http://localhost:8788/mcp
```
---
## Help Channels
- Cloudflare Discord
- GitHub discussions on cloudflare/ai repository

View File

@@ -0,0 +1,338 @@
# Securing MCP Servers
MCP servers require authentication to ensure only trusted users can access them. The MCP specification uses OAuth 2.1 for authentication between clients and servers.
Cloudflare's `workers-oauth-provider` handles token management, client registration, and access token validation automatically.
## Basic Setup
```typescript
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
import { createMcpHandler } from "agents/mcp";
const apiHandler = {
async fetch(request: Request, env: unknown, ctx: ExecutionContext) {
return createMcpHandler(server)(request, env, ctx);
}
};
export default new OAuthProvider({
authorizeEndpoint: "/authorize",
tokenEndpoint: "/oauth/token",
clientRegistrationEndpoint: "/oauth/register",
apiRoute: "/mcp",
apiHandler: apiHandler,
defaultHandler: AuthHandler
});
```
## Proxy Server Pattern
MCP servers often act as OAuth clients too. Your server sits between Claude Desktop and a third-party API like GitHub. To Claude, you're a server. To GitHub, you're a client. This lets users authenticate with their GitHub credentials.
Building a secure proxy server requires careful attention to several security concerns.
---
## Security Requirements
### Redirect URI Validation
The `workers-oauth-provider` validates that `redirect_uri` in authorization requests matches registered URIs. This prevents attackers from redirecting authorization codes to malicious endpoints.
### Consent Dialog
When proxying to third-party providers, implement your own consent dialog before forwarding users upstream. This prevents the "confused deputy" problem where attackers exploit cached consent.
Your consent dialog should:
- Identify the requesting MCP client by name
- Display the specific scopes being requested
---
## CSRF Protection
Prevent attackers from tricking users into approving malicious OAuth clients. Use a random token stored in a secure cookie.
```typescript
// Generate token when showing consent form
function generateCSRFProtection(): { token: string; setCookie: string } {
const token = crypto.randomUUID();
const setCookie = `__Host-CSRF_TOKEN=${token}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`;
return { token, setCookie };
}
// Validate token when user approves
function validateCSRFToken(formData: FormData, request: Request): { clearCookie: string } {
const tokenFromForm = formData.get("csrf_token");
const cookieHeader = request.headers.get("Cookie") || "";
const tokenFromCookie = cookieHeader
.split(";")
.find((c) => c.trim().startsWith("__Host-CSRF_TOKEN="))
?.split("=")[1];
if (!tokenFromForm || !tokenFromCookie || tokenFromForm !== tokenFromCookie) {
throw new Error("CSRF token mismatch");
}
return {
clearCookie: `__Host-CSRF_TOKEN=; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=0`
};
}
```
Include the token as a hidden form field:
```html
<input type="hidden" name="csrf_token" value="${csrfToken}" />
```
---
## Input Sanitization
Client-controlled content (names, logos, URIs) can execute malicious scripts if not sanitized. Treat all client metadata as untrusted.
```typescript
function sanitizeText(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function sanitizeUrl(url: string): string {
if (!url) return "";
try {
const parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) {
return "";
}
return url;
} catch {
return "";
}
}
```
**Required protections:**
- Client names/descriptions: HTML-escape before rendering
- Logo URLs: Allow only `http:` and `https:` schemes
- Client URIs: Same as logo URLs
- Scopes: Treat as text, HTML-escape
---
## Content Security Policy
CSP headers block dangerous content and provide defense in depth.
```typescript
function buildSecurityHeaders(setCookie: string, nonce?: string): HeadersInit {
const cspDirectives = [
"default-src 'none'",
"script-src 'self'" + (nonce ? ` 'nonce-${nonce}'` : ""),
"style-src 'self' 'unsafe-inline'",
"img-src 'self' https:",
"font-src 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"connect-src 'self'"
].join("; ");
return {
"Content-Security-Policy": cspDirectives,
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Content-Type": "text/html; charset=utf-8",
"Set-Cookie": setCookie
};
}
```
---
## State Management
Ensure the same user that hits authorize reaches the callback. Use a random state token stored in KV with short expiration.
```typescript
// Create state before redirecting to upstream provider
async function createOAuthState(
oauthReqInfo: AuthRequest,
kv: KVNamespace
): Promise<{ stateToken: string }> {
const stateToken = crypto.randomUUID();
await kv.put(`oauth:state:${stateToken}`, JSON.stringify(oauthReqInfo), {
expirationTtl: 600
});
return { stateToken };
}
// Bind state to browser session via hashed cookie
async function bindStateToSession(stateToken: string): Promise<{ setCookie: string }> {
const encoder = new TextEncoder();
const data = encoder.encode(stateToken);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return {
setCookie: `__Host-CONSENTED_STATE=${hashHex}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`
};
}
// Validate in callback - check both KV and session cookie
async function validateOAuthState(
request: Request,
kv: KVNamespace
): Promise<{ oauthReqInfo: AuthRequest; clearCookie: string }> {
const url = new URL(request.url);
const stateFromQuery = url.searchParams.get("state");
if (!stateFromQuery) {
throw new Error("Missing state parameter");
}
// Check KV
const storedDataJson = await kv.get(`oauth:state:${stateFromQuery}`);
if (!storedDataJson) {
throw new Error("Invalid or expired state");
}
// Check session cookie matches
const cookieHeader = request.headers.get("Cookie") || "";
const consentedStateHash = cookieHeader
.split(";")
.find((c) => c.trim().startsWith("__Host-CONSENTED_STATE="))
?.split("=")[1];
if (!consentedStateHash) {
throw new Error("Missing session binding cookie");
}
// Hash state and compare
const encoder = new TextEncoder();
const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(stateFromQuery));
const stateHash = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
if (stateHash !== consentedStateHash) {
throw new Error("State token does not match session");
}
await kv.delete(`oauth:state:${stateFromQuery}`);
return {
oauthReqInfo: JSON.parse(storedDataJson),
clearCookie: `__Host-CONSENTED_STATE=; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=0`
};
}
```
---
## Approved Clients Registry
Maintain a registry of approved client IDs per user. Store in a cryptographically signed cookie with HMAC-SHA256.
```typescript
export async function addApprovedClient(
request: Request,
clientId: string,
cookieSecret: string
): Promise<string> {
const existingClients = await getApprovedClientsFromCookie(request, cookieSecret) || [];
const updatedClients = Array.from(new Set([...existingClients, clientId]));
const payload = JSON.stringify(updatedClients);
const signature = await signData(payload, cookieSecret);
const cookieValue = `${signature}.${btoa(payload)}`;
return `__Host-APPROVED_CLIENTS=${cookieValue}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=2592000`;
}
```
When reading the cookie, verify the signature before trusting data. If client isn't approved, show consent dialog.
---
## Cookie Security
### Why `__Host-` prefix?
The `__Host-` prefix prevents subdomain attacks on `*.workers.dev` domains. Requirements:
- Must have `Secure` flag (HTTPS only)
- Must have `Path=/`
- Must not have `Domain` attribute
Without this prefix, an attacker on `evil.workers.dev` could set cookies for your `mcp-server.workers.dev` domain.
### Multiple OAuth Providers
If running multiple OAuth flows on the same domain, namespace your cookies:
- `__Host-CSRF_TOKEN_GITHUB` vs `__Host-CSRF_TOKEN_GOOGLE`
- `__Host-APPROVED_CLIENTS_GITHUB` vs `__Host-APPROVED_CLIENTS_GOOGLE`
---
## Inline JavaScript
If your consent dialog needs inline JavaScript, use data attributes and nonces:
```typescript
const nonce = crypto.randomUUID();
const html = `
<script nonce="${nonce}" data-redirect-url="${sanitizeUrl(redirectUrl)}">
setTimeout(() => {
const script = document.querySelector('script[data-redirect-url]');
window.location.href = script.dataset.redirectUrl;
}, 2000);
</script>
`;
return new Response(html, {
headers: buildSecurityHeaders(setCookie, nonce)
});
```
Data attributes store user-controlled data separately from executable code. Nonces with CSP allow your specific script while blocking injected scripts.
---
## Provider-Specific Setup
### GitHub
1. Create OAuth App at github.com/settings/developers
2. Set callback URL: `https://[worker].workers.dev/callback`
3. Store secrets:
```bash
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET
```
### Google
1. Create OAuth Client at console.cloud.google.com/apis/credentials
2. Set authorized redirect URI
3. Scopes: `openid email profile`
### Auth0
1. Create Regular Web Application in Auth0 Dashboard
2. Set allowed callback URLs
3. Endpoints: `https://${AUTH0_DOMAIN}/authorize`, `/oauth/token`, `/userinfo`
---
## References
- [MCP Authorization Spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/draft/basic/security_best_practices)
- [RFC 9700 - OAuth Security](https://www.rfc-editor.org/rfc/rfc9700)

View File

@@ -0,0 +1,317 @@
# MCP Server Troubleshooting
Common errors and solutions for MCP servers on Cloudflare.
## Connection Issues
### "Failed to connect to MCP server"
**Symptoms:** Client cannot establish connection to deployed server.
**Causes & Solutions:**
1. **Wrong URL path**
```
# Wrong
https://my-server.workers.dev/
# Correct
https://my-server.workers.dev/mcp
```
2. **Worker not deployed**
```bash
wrangler deployments list
# If empty, deploy first:
wrangler deploy
```
3. **Worker crashed on startup**
```bash
wrangler tail
# Check for initialization errors
```
### "WebSocket connection failed"
MCP uses SSE (Server-Sent Events), not WebSockets. Ensure your client is configured for SSE transport:
```json
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-remote", "https://my-server.workers.dev/mcp"]
}
}
}
```
### CORS Errors in Browser
If calling from browser-based client:
```typescript
// Add CORS headers to your worker
export default {
async fetch(request: Request, env: Env) {
// Handle preflight
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
const response = await handleRequest(request, env);
// Add CORS headers to response
const headers = new Headers(response.headers);
headers.set("Access-Control-Allow-Origin", "*");
return new Response(response.body, {
status: response.status,
headers,
});
},
};
```
## Tool Errors
### "Tool not found: [tool_name]"
**Causes:**
1. Tool not registered in `init()`
2. Tool name mismatch (case-sensitive)
3. `init()` threw an error before registering tool
**Debug:**
```typescript
async init() {
console.log("Registering tools...");
this.server.tool("my_tool", { ... }, async () => { ... });
console.log("Tools registered:", this.server.listTools());
}
```
Check logs: `wrangler tail`
### "Invalid parameters for tool"
Zod validation failed. Check parameter schema:
```typescript
// Schema expects number, client sent string
this.server.tool(
"calculate",
{ value: z.number() }, // Client must send number, not "123"
async ({ value }) => { ... }
);
// Fix: Coerce string to number
this.server.tool(
"calculate",
{ value: z.coerce.number() }, // "123" → 123
async ({ value }) => { ... }
);
```
### Tool Timeout
Workers have CPU time limits (10-30ms for free, longer for paid). For long operations:
```typescript
this.server.tool(
"long_operation",
{ ... },
async (params) => {
// Break into smaller chunks
// Or use Queues/Durable Objects for background work
// Don't do this:
// await sleep(5000); // Will timeout
return { content: [{ type: "text", text: "Queued for processing" }] };
}
);
```
## Authentication Errors
### "401 Unauthorized"
OAuth token missing or expired.
1. **Check client is handling OAuth flow**
2. **Verify secrets are set:**
```bash
wrangler secret list
# Should show GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
```
3. **Check KV namespace exists:**
```bash
wrangler kv namespace list
# Should show OAUTH_KV
```
### "Invalid redirect_uri"
OAuth callback URL doesn't match app configuration.
**Local development:**
- OAuth app callback: `http://localhost:8788/callback`
**Production:**
- OAuth app callback: `https://[worker-name].[account].workers.dev/callback`
Must match EXACTLY (including trailing slash or lack thereof).
### "State mismatch" / CSRF Error
State parameter validation failed.
1. **Clear browser cookies and retry**
2. **Check KV is storing state:**
```typescript
// In your auth handler
console.log("Storing state:", state);
await env.OAUTH_KV.put(`state:${state}`, "1", { expirationTtl: 600 });
```
3. **Verify same domain for all requests**
## Binding Errors
### "Binding not found: [BINDING_NAME]"
Binding not in `wrangler.toml` or not deployed.
```toml
# wrangler.toml
[[d1_databases]]
binding = "DB" # Must match env.DB in code
database_name = "mydb"
database_id = "xxx-xxx"
```
After adding bindings: `wrangler deploy`
### "D1_ERROR: no such table"
Migrations not applied.
```bash
# Local
wrangler d1 migrations apply DB_NAME --local
# Production
wrangler d1 migrations apply DB_NAME
```
### Durable Object Not Found
```toml
# wrangler.toml must have:
[durable_objects]
bindings = [{ name = "MCP", class_name = "MyMCP" }]
[[migrations]]
tag = "v1"
new_classes = ["MyMCP"]
```
And class must be exported:
```typescript
export { MyMCP }; // Don't forget this!
```
## Deployment Errors
### "Class MyMCP is not exported"
```typescript
// src/index.ts - Must export the class
export { MyMCP } from "./mcp";
// OR in same file
export class MyMCP extends McpAgent { ... }
```
### "Migration required"
New Durable Object class needs migration:
```toml
# Add to wrangler.toml
[[migrations]]
tag = "v2" # Increment version
new_classes = ["NewClassName"]
# Or for renames:
# renamed_classes = [{ from = "OldName", to = "NewName" }]
```
### Build Errors
```bash
# Clear cache and rebuild
rm -rf node_modules .wrangler
npm install
wrangler deploy
```
## Debugging Tips
### Enable Verbose Logging
```typescript
export class MyMCP extends McpAgent {
async init() {
console.log("MCP Server initializing...");
console.log("Environment:", Object.keys(this.env));
this.server.tool("test", {}, async () => {
console.log("Test tool called");
return { content: [{ type: "text", text: "OK" }] };
});
console.log("Tools registered");
}
}
```
View logs:
```bash
wrangler tail --format pretty
```
### Test Locally First
```bash
npm start
npx @modelcontextprotocol/inspector@latest
```
Always verify tools work locally before deploying.
### Check Worker Health
```bash
# List deployments
wrangler deployments list
# View recent logs
wrangler tail
# Check worker status
curl -I https://your-worker.workers.dev/mcp
```

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/expo/skills/tree/main/plugins/expo-app-design/skills/building-native-ui",
"type": "github-subdir",
"installed_at": "2026-01-30T02:26:49.540522331Z",
"repo_url": "https://github.com/expo/skills.git",
"subdir": "plugins/expo-app-design/skills/building-native-ui",
"version": "b631a60"
}

315
building-native-ui/SKILL.md Normal file
View File

@@ -0,0 +1,315 @@
---
name: building-native-ui
description: Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
version: 1.0.0
license: MIT
---
# Expo UI Guidelines
## References
Consult these resources as needed:
- ./references/route-structure.md -- Route file conventions, dynamic routes, query parameters, groups, and folder organization
- ./references/tabs.md -- Native tab bar with NativeTabs, migration from JS tabs, iOS 26 features
- ./references/icons.md -- SF Symbols with expo-symbols, common icon names, animations, and weights
- ./references/controls.md -- Native iOS controls: Switch, Slider, SegmentedControl, DateTimePicker, Picker
- ./references/visual-effects.md -- Blur effects with expo-blur and liquid glass with expo-glass-effect
- ./references/animations.md -- Reanimated animations: entering, exiting, layout, scroll-driven, and gestures
- ./references/search.md -- Search bar integration with headers, useSearch hook, and filtering patterns
- ./references/gradients.md -- CSS gradients using experimental_backgroundImage (New Architecture only)
- ./references/media.md -- Media handling for Expo Router including camera, audio, video, and file saving
- ./references/storage.md -- Data storage patterns including SQLite, AsyncStorage, and SecureStore
- ./references/webgpu-three.md -- 3D graphics, games, and GPU-powered visualizations with WebGPU and Three.js
- ./references/toolbars-and-headers.md -- Customizing stack headers and toolbar with buttons, menus, and search bars in expo-router app. Available only on iOS.
## Running the App
**CRITICAL: Always try Expo Go first before creating custom builds.**
Most Expo apps work in Expo Go without any custom native code. Before running `npx expo run:ios` or `npx expo run:android`:
1. **Start with Expo Go**: Run `npx expo start` and scan the QR code with Expo Go
2. **Check if features work**: Test your app thoroughly in Expo Go
3. **Only create custom builds when required** - see below
### When Custom Builds Are Required
You need `npx expo run:ios/android` or `eas build` ONLY when using:
- **Local Expo modules** (custom native code in `modules/`)
- **Apple targets** (widgets, app clips, extensions via `@bacons/apple-targets`)
- **Third-party native modules** not included in Expo Go
- **Custom native configuration** that can't be expressed in `app.json`
### When Expo Go Works
Expo Go supports a huge range of features out of the box:
- All `expo-*` packages (camera, location, notifications, etc.)
- Expo Router navigation
- Most UI libraries (reanimated, gesture handler, etc.)
- Push notifications, deep links, and more
**If you're unsure, try Expo Go first.** Creating custom builds adds complexity, slower iteration, and requires Xcode/Android Studio setup.
## Code Style
- Be cautious of unterminated strings. Ensure nested backticks are escaped; never forget to escape quotes correctly.
- Always use import statements at the top of the file.
- Always use kebab-case for file names, e.g. `comment-card.tsx`
- Always remove old route files when moving or restructuring navigation
- Never use special characters in file names
- Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors.
## Routes
See `./references/route-structure.md` for detailed route conventions.
- Routes belong in the `app` directory.
- Never co-locate components, types, or utilities in the app directory. This is an anti-pattern.
- Ensure the app always has a route that matches "/", it may be inside a group route.
## Library Preferences
- Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage
- Never use legacy expo-permissions
- `expo-audio` not `expo-av`
- `expo-video` not `expo-av`
- `expo-symbols` not `@expo/vector-icons`
- `react-native-safe-area-context` not react-native SafeAreaView
- `process.env.EXPO_OS` not `Platform.OS`
- `React.use` not `React.useContext`
- `expo-image` Image component instead of intrinsic element `img`
- `expo-glass-effect` for liquid glass backdrops
## Responsiveness
- Always wrap root component in a scroll view for responsiveness
- Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>` for smarter safe area insets
- `contentInsetAdjustmentBehavior="automatic"` should be applied to FlatList and SectionList as well
- Use flexbox instead of Dimensions API
- ALWAYS prefer `useWindowDimensions` over `Dimensions.get()` to measure screen size
## Behavior
- Use expo-haptics conditionally on iOS to make more delightful experiences
- Use views with built-in haptics like `<Switch />` from React Native and `@react-native-community/datetimepicker`
- When a route belongs to a Stack, its first child should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"` set
- Prefer `headerSearchBarOptions` in Stack.Screen options to add a search bar
- Use the `<Text selectable />` prop on text containing data that could be copied
- Consider formatting large numbers like 1.4M or 38k
- Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component
# Styling
Follow Apple Human Interface Guidelines.
## General Styling Rules
- Prefer flex gap over margin and padding styles
- Prefer padding over margin where possible
- Always account for safe area, either with stack headers, tabs, or ScrollView/FlatList `contentInsetAdjustmentBehavior="automatic"`
- Ensure both top and bottom safe area insets are accounted for
- Inline styles not StyleSheet.create unless reusing styles is faster
- Add entering and exiting animations for state changes
- Use `{ borderCurve: 'continuous' }` for rounded corners unless creating a capsule shape
- ALWAYS use a navigation stack title instead of a custom text element on the page
- When padding a ScrollView, use `contentContainerStyle` padding and gap instead of padding on the ScrollView itself (reduces clipping)
- CSS and Tailwind are not supported - use inline styles
## Text Styling
- Add the `selectable` prop to every `<Text/>` element displaying important data or error messages
- Counters should use `{ fontVariant: 'tabular-nums' }` for alignment
## Shadows
Use CSS `boxShadow` style prop. NEVER use legacy React Native shadow or elevation styles.
```tsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
```
'inset' shadows are supported.
# Navigation
## Link
Use `<Link href="/path" />` from 'expo-router' for navigation between routes.
```tsx
import { Link } from 'expo-router';
// Basic link
<Link href="/path" />
// Wrapping custom components
<Link href="/path" asChild>
<Pressable>...</Pressable>
</Link>
```
Whenever possible, include a `<Link.Preview>` to follow iOS conventions. Add context menus and previews frequently to enhance navigation.
## Stack
- ALWAYS use `_layout.tsx` files to define stacks
- Use Stack from 'expo-router/stack' for native navigation stacks
### Page Title
Set the page title in Stack.Screen options:
```tsx
<Stack.Screen options={{ title: "Home" }} />
```
## Context Menus
Add long press context menus to Link components:
```tsx
import { Link } from "expo-router";
<Link href="/settings" asChild>
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Menu>
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={handleSharePress}
/>
<Link.MenuAction
title="Block"
icon="nosign"
destructive
onPress={handleBlockPress}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} />
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => {}}
/>
</Link.Menu>
</Link.Menu>
</Link>;
```
## Link Previews
Use link previews frequently to enhance navigation:
```tsx
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
</Link>
```
Link preview can be used with context menus.
## Modal
Present a screen as a modal:
```tsx
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
```
Prefer this to building a custom modal component.
## Sheet
Present a screen as a dynamic form sheet:
```tsx
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>
```
- Using `contentStyle: { backgroundColor: "transparent" }` makes the background liquid glass on iOS 26+.
## Common route structure
A standard app layout with tabs and stacks inside each tab:
```
app/
_layout.tsx — <NativeTabs />
(index,search)/
_layout.tsx — <Stack />
index.tsx — Main list
search.tsx — Search view
```
```tsx
// app/_layout.tsx
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
import { Theme } from "../components/theme";
export default function Layout() {
return (
<Theme>
<NativeTabs>
<NativeTabs.Trigger name="(index)">
<Icon sf="list.dash" />
<Label>Items</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search" />
</NativeTabs>
</Theme>
);
}
```
Create a shared group route so both tabs can push common screens:
```tsx
// app/(index,search)/_layout.tsx
import { Stack } from "expo-router/stack";
import { PlatformColor } from "react-native";
export default function Layout({ segment }) {
const screen = segment.match(/\((.*)\)/)?.[1]!;
const titles: Record<string, string> = { index: "Items", search: "Search" };
return (
<Stack
screenOptions={{
headerTransparent: true,
headerShadowVisible: false,
headerLargeTitleShadowVisible: false,
headerLargeStyle: { backgroundColor: "transparent" },
headerTitleStyle: { color: PlatformColor("label") },
headerLargeTitle: true,
headerBlurEffect: "none",
headerBackButtonDisplayMode: "minimal",
}}
>
<Stack.Screen name={screen} options={{ title: titles[screen] }} />
<Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
</Stack>
);
}
```

View File

@@ -0,0 +1,220 @@
# Animations
Use Reanimated v4. Avoid React Native's built-in Animated API.
## Entering and Exiting Animations
Use Animated.View with entering and exiting animations. Layout animations can animate state changes.
```tsx
import Animated, {
FadeIn,
FadeOut,
LinearTransition,
} from "react-native-reanimated";
function App() {
return (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
layout={LinearTransition}
/>
);
}
```
## On-Scroll Animations
Create high-performance scroll animations using Reanimated's hooks:
```tsx
import Animated, {
useAnimatedRef,
useScrollViewOffset,
useAnimatedStyle,
interpolate,
} from "react-native-reanimated";
function Page() {
const ref = useAnimatedRef();
const scroll = useScrollViewOffset(ref);
const style = useAnimatedStyle(() => ({
opacity: interpolate(scroll.value, [0, 30], [0, 1], "clamp"),
}));
return (
<Animated.ScrollView ref={ref}>
<Animated.View style={style} />
</Animated.ScrollView>
);
}
```
## Common Animation Presets
### Entering Animations
- `FadeIn`, `FadeInUp`, `FadeInDown`, `FadeInLeft`, `FadeInRight`
- `SlideInUp`, `SlideInDown`, `SlideInLeft`, `SlideInRight`
- `ZoomIn`, `ZoomInUp`, `ZoomInDown`
- `BounceIn`, `BounceInUp`, `BounceInDown`
### Exiting Animations
- `FadeOut`, `FadeOutUp`, `FadeOutDown`, `FadeOutLeft`, `FadeOutRight`
- `SlideOutUp`, `SlideOutDown`, `SlideOutLeft`, `SlideOutRight`
- `ZoomOut`, `ZoomOutUp`, `ZoomOutDown`
- `BounceOut`, `BounceOutUp`, `BounceOutDown`
### Layout Animations
- `LinearTransition` — Smooth linear interpolation
- `SequencedTransition` — Sequenced property changes
- `FadingTransition` — Fade between states
## Customizing Animations
```tsx
<Animated.View
entering={FadeInDown.duration(500).delay(200)}
exiting={FadeOut.duration(300)}
/>
```
### Modifiers
```tsx
// Duration in milliseconds
FadeIn.duration(300);
// Delay before starting
FadeIn.delay(100);
// Spring physics
FadeIn.springify();
FadeIn.springify().damping(15).stiffness(100);
// Easing curves
FadeIn.easing(Easing.bezier(0.25, 0.1, 0.25, 1));
// Chaining
FadeInDown.duration(400).delay(200).springify();
```
## Shared Value Animations
For imperative control over animations:
```tsx
import {
useSharedValue,
withSpring,
withTiming,
} from "react-native-reanimated";
const offset = useSharedValue(0);
// Spring animation
offset.value = withSpring(100);
// Timing animation
offset.value = withTiming(100, { duration: 300 });
// Use in styles
const style = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
```
## Gesture Animations
Combine with React Native Gesture Handler:
```tsx
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from "react-native-reanimated";
function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const gesture = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX;
translateY.value = e.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const style = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.box, style]} />
</GestureDetector>
);
}
```
## Keyboard Animations
Animate with keyboard height changes:
```tsx
import Animated, {
useAnimatedKeyboard,
useAnimatedStyle,
} from "react-native-reanimated";
function KeyboardAwareView() {
const keyboard = useAnimatedKeyboard();
const style = useAnimatedStyle(() => ({
paddingBottom: keyboard.height.value,
}));
return <Animated.View style={style}>{/* content */}</Animated.View>;
}
```
## Staggered List Animations
Animate list items with delays:
```tsx
{
items.map((item, index) => (
<Animated.View
key={item.id}
entering={FadeInUp.delay(index * 50)}
exiting={FadeOutUp}
>
<ListItem item={item} />
</Animated.View>
));
}
```
## Best Practices
- Add entering and exiting animations for state changes
- Use layout animations when items are added/removed from lists
- Use `useAnimatedStyle` for scroll-driven animations
- Prefer `interpolate` with "clamp" for bounded values
- You can't pass PlatformColors to reanimated views or styles; use static colors instead
- Keep animations under 300ms for responsive feel
- Use spring animations for natural movement
- Avoid animating layout properties (width, height) when possible — prefer transforms

View File

@@ -0,0 +1,270 @@
# Native Controls
Native iOS controls provide built-in haptics, accessibility, and platform-appropriate styling.
## Switch
Use for binary on/off settings. Has built-in haptics.
```tsx
import { Switch } from "react-native";
import { useState } from "react";
const [enabled, setEnabled] = useState(false);
<Switch value={enabled} onValueChange={setEnabled} />;
```
### Customization
```tsx
<Switch
value={enabled}
onValueChange={setEnabled}
trackColor={{ false: "#767577", true: "#81b0ff" }}
thumbColor={enabled ? "#f5dd4b" : "#f4f3f4"}
ios_backgroundColor="#3e3e3e"
/>
```
## Segmented Control
Use for non-navigational tabs or mode selection. Avoid changing default colors.
```tsx
import SegmentedControl from "@react-native-segmented-control/segmented-control";
import { useState } from "react";
const [index, setIndex] = useState(0);
<SegmentedControl
values={["All", "Active", "Done"]}
selectedIndex={index}
onChange={({ nativeEvent }) => setIndex(nativeEvent.selectedSegmentIndex)}
/>;
```
### Rules
- Maximum 4 options — use a picker for more
- Keep labels short (1-2 words)
- Avoid custom colors — native styling adapts to dark mode
### With Icons (iOS 14+)
```tsx
<SegmentedControl
values={[
{ label: "List", icon: "list.bullet" },
{ label: "Grid", icon: "square.grid.2x2" },
]}
selectedIndex={index}
onChange={({ nativeEvent }) => setIndex(nativeEvent.selectedSegmentIndex)}
/>
```
## Slider
Continuous value selection.
```tsx
import Slider from "@react-native-community/slider";
import { useState } from "react";
const [value, setValue] = useState(0.5);
<Slider
value={value}
onValueChange={setValue}
minimumValue={0}
maximumValue={1}
/>;
```
### Customization
```tsx
<Slider
value={value}
onValueChange={setValue}
minimumValue={0}
maximumValue={100}
step={1}
minimumTrackTintColor="#007AFF"
maximumTrackTintColor="#E5E5EA"
thumbTintColor="#007AFF"
/>
```
### Discrete Steps
```tsx
<Slider
value={value}
onValueChange={setValue}
minimumValue={0}
maximumValue={10}
step={1}
/>
```
## Date/Time Picker
Compact pickers with popovers. Has built-in haptics.
```tsx
import DateTimePicker from "@react-native-community/datetimepicker";
import { useState } from "react";
const [date, setDate] = useState(new Date());
<DateTimePicker
value={date}
onChange={(event, selectedDate) => {
if (selectedDate) setDate(selectedDate);
}}
mode="datetime"
/>;
```
### Modes
- `date` — Date only
- `time` — Time only
- `datetime` — Date and time
### Display Styles
```tsx
// Compact inline (default)
<DateTimePicker value={date} mode="date" />
// Spinner wheel
<DateTimePicker
value={date}
mode="date"
display="spinner"
style={{ width: 200, height: 150 }}
/>
// Full calendar
<DateTimePicker value={date} mode="date" display="inline" />
```
### Time Intervals
```tsx
<DateTimePicker
value={date}
mode="time"
minuteInterval={15}
/>
```
### Min/Max Dates
```tsx
<DateTimePicker
value={date}
mode="date"
minimumDate={new Date(2020, 0, 1)}
maximumDate={new Date(2030, 11, 31)}
/>
```
## Stepper
Increment/decrement numeric values.
```tsx
import { Stepper } from "react-native";
import { useState } from "react";
const [count, setCount] = useState(0);
<Stepper
value={count}
onValueChange={setCount}
minimumValue={0}
maximumValue={10}
/>;
```
## TextInput
Native text input with various keyboard types.
```tsx
import { TextInput } from "react-native";
<TextInput
placeholder="Enter text..."
placeholderTextColor="#999"
style={{
padding: 12,
fontSize: 16,
borderRadius: 8,
backgroundColor: "#f0f0f0",
}}
/>
```
### Keyboard Types
```tsx
// Email
<TextInput keyboardType="email-address" autoCapitalize="none" />
// Phone
<TextInput keyboardType="phone-pad" />
// Number
<TextInput keyboardType="numeric" />
// Password
<TextInput secureTextEntry />
// Search
<TextInput
returnKeyType="search"
enablesReturnKeyAutomatically
/>
```
### Multiline
```tsx
<TextInput
multiline
numberOfLines={4}
textAlignVertical="top"
style={{ minHeight: 100 }}
/>
```
## Picker (Wheel)
For selection from many options (5+ items).
```tsx
import { Picker } from "@react-native-picker/picker";
import { useState } from "react";
const [selected, setSelected] = useState("js");
<Picker selectedValue={selected} onValueChange={setSelected}>
<Picker.Item label="JavaScript" value="js" />
<Picker.Item label="TypeScript" value="ts" />
<Picker.Item label="Python" value="py" />
<Picker.Item label="Go" value="go" />
</Picker>;
```
## Best Practices
- **Haptics**: Switch and DateTimePicker have built-in haptics — don't add extra
- **Accessibility**: Native controls have proper accessibility labels by default
- **Dark Mode**: Avoid custom colors — native styling adapts automatically
- **Spacing**: Use consistent padding around controls (12-16pt)
- **Labels**: Place labels above or to the left of controls
- **Grouping**: Group related controls in sections with headers

View File

@@ -0,0 +1,227 @@
# Form Sheets in Expo Router
This skill covers implementing form sheets with footers using Expo Router's Stack navigator and react-native-screens.
## Overview
Form sheets are modal presentations that appear as a card sliding up from the bottom of the screen. They're ideal for:
- Quick actions and confirmations
- Settings panels
- Login/signup flows
- Action sheets with custom content
**Requirements:**
- Expo Router Stack navigator
## Basic Usage
### Form Sheet with Footer
Configure the Stack.Screen with transparent backgrounds and sheet presentation:
```tsx
// app/_layout.tsx
import { Stack } from "expo-router";
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" />
<Stack.Screen
name="about"
options={{
presentation: "formSheet",
sheetAllowedDetents: [0.25],
headerTransparent: true,
contentStyle: { backgroundColor: "transparent" },
sheetGrabberVisible: true,
}}
>
<Stack.Header style={{ backgroundColor: "transparent" }}></Stack.Header>
</Stack.Screen>
</Stack>
);
}
```
### Form Sheet Screen Content
> Requires Expo SDK 55 or later.
Use `flex: 1` to allow the content to fill available space, enabling footer positioning:
```tsx
// app/about.tsx
import { View, Text, StyleSheet } from "react-native";
export default function AboutSheet() {
return (
<View style={styles.container}>
{/* Main content */}
<View style={styles.content}>
<Text>Sheet Content</Text>
</View>
{/* Footer - stays at bottom */}
<View style={styles.footer}>
<Text>Footer Content</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
padding: 16,
},
footer: {
padding: 16,
},
});
```
## Key Options
| Option | Type | Description |
| --------------------- | ---------- | ----------------------------------------------------------- |
| `presentation` | `string` | Set to `'formSheet'` for sheet presentation |
| `sheetGrabberVisible` | `boolean` | Shows the drag handle at the top of the sheet |
| `sheetAllowedDetents` | `number[]` | Array of detent heights (0-1 range, e.g., `[0.25]` for 25%) |
| `headerTransparent` | `boolean` | Makes header background transparent |
| `contentStyle` | `object` | Style object for the screen content container |
| `title` | `string` | Screen title (set to `''` for no title) |
## Common Detent Values
- `[0.25]` - Quarter sheet (compact actions)
- `[0.5]` - Half sheet (medium content)
- `[0.75]` - Three-quarter sheet (detailed forms)
- `[0.25, 0.5, 1]` - Multiple stops (expandable sheet)
## Complete Example
```tsx
// _layout.tsx
import { Stack } from "expo-router";
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Home" }} />
<Stack.Screen
name="confirm"
options={{
contentStyle: { backgroundColor: "transparent" },
presentation: "formSheet",
title: "",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.25],
headerTransparent: true,
}}
>
<Stack.Header style={{ backgroundColor: "transparent" }}>
<Stack.Header.Right />
</Stack.Header>
</Stack.Screen>
</Stack>
);
}
```
```tsx
// app/confirm.tsx
import { View, Text, Pressable, StyleSheet } from "react-native";
import { router } from "expo-router";
export default function ConfirmSheet() {
return (
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Confirm Action</Text>
<Text style={styles.description}>
Are you sure you want to proceed?
</Text>
</View>
<View style={styles.footer}>
<Pressable style={styles.cancelButton} onPress={() => router.back()}>
<Text style={styles.cancelText}>Cancel</Text>
</Pressable>
<Pressable style={styles.confirmButton} onPress={() => router.back()}>
<Text style={styles.confirmText}>Confirm</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
alignItems: "center",
justifyContent: "center",
},
title: {
fontSize: 18,
fontWeight: "600",
marginBottom: 8,
},
description: {
fontSize: 14,
color: "#666",
textAlign: "center",
},
footer: {
flexDirection: "row",
padding: 16,
gap: 12,
},
cancelButton: {
flex: 1,
padding: 14,
borderRadius: 10,
backgroundColor: "#f0f0f0",
alignItems: "center",
},
cancelText: {
fontSize: 16,
fontWeight: "500",
},
confirmButton: {
flex: 1,
padding: 14,
borderRadius: 10,
backgroundColor: "#007AFF",
alignItems: "center",
},
confirmText: {
fontSize: 16,
fontWeight: "500",
color: "white",
},
});
```
## Troubleshooting
### Content not filling sheet
Make sure the root View uses `flex: 1`:
```tsx
<View style={{ flex: 1 }}>{/* content */}</View>
```
### Sheet background showing through
Set `contentStyle: { backgroundColor: 'transparent' }` in options and style your content container with the desired background color instead.

View File

@@ -0,0 +1,106 @@
# CSS Gradients
> **New Architecture Only**: CSS gradients require React Native's New Architecture (Fabric). They are not available in the old architecture or Expo Go.
Use CSS gradients with the `experimental_backgroundImage` style property.
## Linear Gradients
```tsx
// Top to bottom
<View style={{
experimental_backgroundImage: 'linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)'
}} />
// Left to right
<View style={{
experimental_backgroundImage: 'linear-gradient(to right, #ff0000 0%, #0000ff 100%)'
}} />
// Diagonal
<View style={{
experimental_backgroundImage: 'linear-gradient(45deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)'
}} />
// Using degrees
<View style={{
experimental_backgroundImage: 'linear-gradient(135deg, transparent 0%, black 100%)'
}} />
```
## Radial Gradients
```tsx
// Circle at center
<View style={{
experimental_backgroundImage: 'radial-gradient(circle at center, rgba(255, 0, 0, 1) 0%, rgba(0, 0, 255, 1) 100%)'
}} />
// Ellipse
<View style={{
experimental_backgroundImage: 'radial-gradient(ellipse at center, #fff 0%, #000 100%)'
}} />
// Positioned
<View style={{
experimental_backgroundImage: 'radial-gradient(circle at top left, #ff0000 0%, transparent 70%)'
}} />
```
## Multiple Gradients
Stack multiple gradients by comma-separating them:
```tsx
<View style={{
experimental_backgroundImage: `
linear-gradient(to bottom, transparent 0%, black 100%),
radial-gradient(circle at top right, rgba(255, 0, 0, 0.5) 0%, transparent 50%)
`
}} />
```
## Common Patterns
### Overlay on Image
```tsx
<View style={{ position: 'relative' }}>
<Image source={{ uri: '...' }} style={{ width: '100%', height: 200 }} />
<View style={{
position: 'absolute',
inset: 0,
experimental_backgroundImage: 'linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, transparent 50%)'
}} />
</View>
```
### Frosted Glass Effect
```tsx
<View style={{
experimental_backgroundImage: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%)',
backdropFilter: 'blur(10px)',
}} />
```
### Button Gradient
```tsx
<Pressable style={{
experimental_backgroundImage: 'linear-gradient(to bottom, #4CAF50 0%, #388E3C 100%)',
padding: 16,
borderRadius: 8,
}}>
<Text style={{ color: 'white', textAlign: 'center' }}>Submit</Text>
</Pressable>
```
## Important Notes
- Do NOT use `expo-linear-gradient` — use CSS gradients instead
- Gradients are strings, not objects
- Use `rgba()` for transparency, or `transparent` keyword
- Color stops use percentages (0%, 50%, 100%)
- Direction keywords: `to top`, `to bottom`, `to left`, `to right`, `to top left`, etc.
- Degree values: `45deg`, `90deg`, `135deg`, etc.

View File

@@ -0,0 +1,213 @@
# Icons (SF Symbols)
Use SF Symbols for native feel. Never use FontAwesome or Ionicons.
## Basic Usage
```tsx
import { SymbolView } from "expo-symbols";
import { PlatformColor } from "react-native";
<SymbolView
tintColor={PlatformColor("label")}
resizeMode="scaleAspectFit"
name="square.and.arrow.down"
style={{ width: 16, height: 16 }}
/>;
```
## Props
```tsx
<SymbolView
name="star.fill" // SF Symbol name (required)
tintColor={PlatformColor("label")} // Icon color
size={24} // Shorthand for width/height
resizeMode="scaleAspectFit" // How to scale
weight="regular" // thin | ultraLight | light | regular | medium | semibold | bold | heavy | black
scale="medium" // small | medium | large
style={{ width: 16, height: 16 }} // Standard style props
/>
```
## Common Icons
### Navigation & Actions
- `house.fill` - home
- `gear` - settings
- `magnifyingglass` - search
- `plus` - add
- `xmark` - close
- `chevron.left` - back
- `chevron.right` - forward
- `arrow.left` - back arrow
- `arrow.right` - forward arrow
### Media
- `play.fill` - play
- `pause.fill` - pause
- `stop.fill` - stop
- `backward.fill` - rewind
- `forward.fill` - fast forward
- `speaker.wave.2.fill` - volume
- `speaker.slash.fill` - mute
### Camera
- `camera` - camera
- `camera.fill` - camera filled
- `arrow.triangle.2.circlepath` - flip camera
- `photo` - gallery/photos
- `bolt` - flash
- `bolt.slash` - flash off
### Communication
- `message` - message
- `message.fill` - message filled
- `envelope` - email
- `envelope.fill` - email filled
- `phone` - phone
- `phone.fill` - phone filled
- `video` - video call
- `video.fill` - video call filled
### Social
- `heart` - like
- `heart.fill` - liked
- `star` - favorite
- `star.fill` - favorited
- `hand.thumbsup` - thumbs up
- `hand.thumbsdown` - thumbs down
- `person` - profile
- `person.fill` - profile filled
- `person.2` - people
- `person.2.fill` - people filled
### Content Actions
- `square.and.arrow.up` - share
- `square.and.arrow.down` - download
- `doc.on.doc` - copy
- `trash` - delete
- `pencil` - edit
- `folder` - folder
- `folder.fill` - folder filled
- `bookmark` - bookmark
- `bookmark.fill` - bookmarked
### Status & Feedback
- `checkmark` - success/done
- `checkmark.circle.fill` - completed
- `xmark.circle.fill` - error/failed
- `exclamationmark.triangle` - warning
- `info.circle` - info
- `questionmark.circle` - help
- `bell` - notification
- `bell.fill` - notification filled
### Misc
- `ellipsis` - more options
- `ellipsis.circle` - more in circle
- `line.3.horizontal` - menu/hamburger
- `slider.horizontal.3` - filters
- `arrow.clockwise` - refresh
- `location` - location
- `location.fill` - location filled
- `map` - map
- `mappin` - pin
- `clock` - time
- `calendar` - calendar
- `link` - link
- `nosign` - block/prohibited
## Animated Symbols
```tsx
<SymbolView
name="checkmark.circle"
animationSpec={{
effect: {
type: "bounce",
direction: "up",
},
}}
/>
```
### Animation Effects
- `bounce` - Bouncy animation
- `pulse` - Pulsing effect
- `variableColor` - Color cycling
- `scale` - Scale animation
```tsx
// Bounce with direction
animationSpec={{
effect: { type: "bounce", direction: "up" } // up | down
}}
// Pulse
animationSpec={{
effect: { type: "pulse" }
}}
// Variable color (multicolor symbols)
animationSpec={{
effect: {
type: "variableColor",
cumulative: true,
reversing: true
}
}}
```
## Symbol Weights
```tsx
// Lighter weights
<SymbolView name="star" weight="ultraLight" />
<SymbolView name="star" weight="thin" />
<SymbolView name="star" weight="light" />
// Default
<SymbolView name="star" weight="regular" />
// Heavier weights
<SymbolView name="star" weight="medium" />
<SymbolView name="star" weight="semibold" />
<SymbolView name="star" weight="bold" />
<SymbolView name="star" weight="heavy" />
<SymbolView name="star" weight="black" />
```
## Symbol Scales
```tsx
<SymbolView name="star" scale="small" />
<SymbolView name="star" scale="medium" /> // default
<SymbolView name="star" scale="large" />
```
## Multicolor Symbols
Some symbols support multiple colors:
```tsx
<SymbolView
name="cloud.sun.rain.fill"
type="multicolor"
/>
```
## Finding Symbol Names
1. Use the SF Symbols app on macOS (free from Apple)
2. Search at https://developer.apple.com/sf-symbols/
3. Symbol names use dot notation: `square.and.arrow.up`
## Best Practices
- Always use SF Symbols over vector icon libraries
- Match symbol weight to nearby text weight
- Use `.fill` variants for selected/active states
- Use PlatformColor for tint to support dark mode
- Keep icons at consistent sizes (16, 20, 24, 32)

View File

@@ -0,0 +1,198 @@
# Media
## Camera
- Hide navigation headers when there's a full screen camera
- Ensure to flip the camera with `mirror` to emulate social apps
- Use liquid glass buttons on cameras
- Icons: `arrow.triangle.2.circlepath` (flip), `photo` (gallery), `bolt` (flash)
- Eagerly request camera permission
- Lazily request media library permission
```tsx
import React, { useRef, useState } from "react";
import { View, TouchableOpacity, Text, Alert } from "react-native";
import { CameraView, CameraType, useCameraPermissions } from "expo-camera";
import * as MediaLibrary from "expo-media-library";
import * as ImagePicker from "expo-image-picker";
import * as Haptics from "expo-haptics";
import { SymbolView } from "expo-symbols";
import { PlatformColor } from "react-native";
import { GlassView } from "expo-glass-effect";
import { useSafeAreaInsets } from "react-native-safe-area-context";
function Camera({ onPicture }: { onPicture: (uri: string) => Promise<void> }) {
const [permission, requestPermission] = useCameraPermissions();
const cameraRef = useRef<CameraView>(null);
const [type, setType] = useState<CameraType>("back");
const { bottom } = useSafeAreaInsets();
if (!permission?.granted) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: PlatformColor("systemBackground") }}>
<Text style={{ color: PlatformColor("label"), padding: 16 }}>Camera access is required</Text>
<GlassView isInteractive tintColor={PlatformColor("systemBlue")} style={{ borderRadius: 12 }}>
<TouchableOpacity onPress={requestPermission} style={{ padding: 12, borderRadius: 12 }}>
<Text style={{ color: "white" }}>Grant Permission</Text>
</TouchableOpacity>
</GlassView>
</View>
);
}
const takePhoto = async () => {
await Haptics.selectionAsync();
if (!cameraRef.current) return;
const photo = await cameraRef.current.takePictureAsync({ quality: 0.8 });
await onPicture(photo.uri);
};
const selectPhoto = async () => {
await Haptics.selectionAsync();
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: "images",
allowsEditing: false,
quality: 0.8,
});
if (!result.canceled && result.assets?.[0]) {
await onPicture(result.assets[0].uri);
}
};
return (
<View style={{ flex: 1, backgroundColor: "black" }}>
<CameraView ref={cameraRef} mirror style={{ flex: 1 }} facing={type} />
<View style={{ position: "absolute", left: 0, right: 0, bottom: bottom, gap: 16, alignItems: "center" }}>
<GlassView isInteractive style={{ padding: 8, borderRadius: 99 }}>
<TouchableOpacity onPress={takePhoto} style={{ width: 64, height: 64, borderRadius: 99, backgroundColor: "white" }} />
</GlassView>
<View style={{ flexDirection: "row", justifyContent: "space-around", paddingHorizontal: 8 }}>
<GlassButton onPress={selectPhoto} icon="photo" />
<GlassButton onPress={() => setType(t => t === "back" ? "front" : "back")} icon="arrow.triangle.2.circlepath" />
</View>
</View>
</View>
);
}
```
## Audio Playback
Use `expo-audio` not `expo-av`:
```tsx
import { useAudioPlayer } from 'expo-audio';
const player = useAudioPlayer({ uri: 'https://stream.nightride.fm/rektory.mp3' });
<Button title="Play" onPress={() => player.play()} />
```
## Audio Recording (Microphone)
```tsx
import {
useAudioRecorder,
AudioModule,
RecordingPresets,
setAudioModeAsync,
useAudioRecorderState,
} from 'expo-audio';
import { useEffect } from 'react';
import { Alert, Button } from 'react-native';
function App() {
const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
const recorderState = useAudioRecorderState(audioRecorder);
const record = async () => {
await audioRecorder.prepareToRecordAsync();
audioRecorder.record();
};
const stop = () => audioRecorder.stop();
useEffect(() => {
(async () => {
const status = await AudioModule.requestRecordingPermissionsAsync();
if (status.granted) {
setAudioModeAsync({ playsInSilentMode: true, allowsRecording: true });
} else {
Alert.alert('Permission to access microphone was denied');
}
})();
}, []);
return (
<Button
title={recorderState.isRecording ? 'Stop' : 'Start'}
onPress={recorderState.isRecording ? stop : record}
/>
);
}
```
## Video Playback
Use `expo-video` not `expo-av`:
```tsx
import { useVideoPlayer, VideoView } from 'expo-video';
import { useEvent } from 'expo';
const videoSource = 'https://example.com/video.mp4';
const player = useVideoPlayer(videoSource, player => {
player.loop = true;
player.play();
});
const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
<VideoView player={player} fullscreenOptions={{}} allowsPictureInPicture />
```
VideoView options:
- `allowsPictureInPicture`: boolean
- `contentFit`: 'contain' | 'cover' | 'fill'
- `nativeControls`: boolean
- `playsInline`: boolean
- `startsPictureInPictureAutomatically`: boolean
## Saving Media
```tsx
import * as MediaLibrary from "expo-media-library";
const { granted } = await MediaLibrary.requestPermissionsAsync();
if (granted) {
await MediaLibrary.saveToLibraryAsync(uri);
}
```
### Saving Base64 Images
`MediaLibrary.saveToLibraryAsync` only accepts local file paths. Save base64 strings to disk first:
```tsx
import { File, Paths } from "expo-file-system/next";
function base64ToLocalUri(base64: string, filename?: string) {
if (!filename) {
const match = base64.match(/^data:(image\/[a-zA-Z]+);base64,/);
const ext = match ? match[1].split("/")[1] : "jpg";
filename = `generated-${Date.now()}.${ext}`;
}
if (base64.startsWith("data:")) base64 = base64.split(",")[1];
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(new ArrayBuffer(len));
for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
const f = new File(Paths.cache, filename);
f.create({ overwrite: true });
f.write(bytes);
return f.uri;
}
```

View File

@@ -0,0 +1,229 @@
# Route Structure
## File Conventions
- Routes belong in the `app` directory
- Use `[]` for dynamic routes, e.g. `[id].tsx`
- Routes can never be named `(foo).tsx` - use `(foo)/index.tsx` instead
- Use `(group)` routes to simplify the public URL structure
- NEVER co-locate components, types, or utilities in the app directory - these should be in separate directories like `components/`, `utils/`, etc.
- The app directory should only contain route and `_layout` files; every file should export a default component
- Ensure the app always has a route that matches "/" so the app is never blank
- ALWAYS use `_layout.tsx` files to define stacks
## Dynamic Routes
Use square brackets for dynamic segments:
```
app/
users/
[id].tsx # Matches /users/123, /users/abc
[id]/
posts.tsx # Matches /users/123/posts
```
### Catch-All Routes
Use `[...slug]` for catch-all routes:
```
app/
docs/
[...slug].tsx # Matches /docs/a, /docs/a/b, /docs/a/b/c
```
## Query Parameters
Access query parameters with the `useLocalSearchParams` hook:
```tsx
import { useLocalSearchParams } from "expo-router";
function Page() {
const { id } = useLocalSearchParams<{ id: string }>();
}
```
For dynamic routes, the parameter name matches the file name:
- `[id].tsx``useLocalSearchParams<{ id: string }>()`
- `[slug].tsx``useLocalSearchParams<{ slug: string }>()`
## Pathname
Access the current pathname with the `usePathname` hook:
```tsx
import { usePathname } from "expo-router";
function Component() {
const pathname = usePathname(); // e.g. "/users/123"
}
```
## Group Routes
Use parentheses for groups that don't affect the URL:
```
app/
(auth)/
login.tsx # URL: /login
register.tsx # URL: /register
(main)/
index.tsx # URL: /
settings.tsx # URL: /settings
```
Groups are useful for:
- Organizing related routes
- Applying different layouts to route groups
- Keeping URLs clean
## Stacks and Tabs Structure
When an app has tabs, the header and title should be set in a Stack that is nested INSIDE each tab. This allows tabs to have their own headers and distinct histories. The root layout should often not have a header.
- Set the 'headerShown' option to false on the tab layout
- Use (group) routes to simplify the public URL structure
- You may need to delete or refactor existing routes to fit this structure
Example structure:
```
app/
_layout.tsx — <Tabs />
(home)/
_layout.tsx — <Stack />
index.tsx — <ScrollView />
(settings)/
_layout.tsx — <Stack />
index.tsx — <ScrollView />
(home,settings)/
info.tsx — <ScrollView /> (shared across tabs)
```
## Array Routes for Multiple Stacks
Use array routes '(index,settings)' to create multiple stacks. This is useful for tabs that need to share screens across stacks.
```
app/
_layout.tsx — <Tabs />
(index,settings)/
_layout.tsx — <Stack />
index.tsx — <ScrollView />
settings.tsx — <ScrollView />
```
This requires a specialized layout with explicit anchor routes:
```tsx
// app/(index,settings)/_layout.tsx
import { useMemo } from "react";
import Stack from "expo-router/stack";
export const unstable_settings = {
index: { anchor: "index" },
settings: { anchor: "settings" },
};
export default function Layout({ segment }: { segment: string }) {
const screen = segment.match(/\((.*)\)/)?.[1]!;
const options = useMemo(() => {
switch (screen) {
case "index":
return { headerRight: () => <></> };
default:
return {};
}
}, [screen]);
return (
<Stack>
<Stack.Screen name={screen} options={options} />
</Stack>
);
}
```
## Complete App Structure Example
```
app/
_layout.tsx — <NativeTabs />
(index,search)/
_layout.tsx — <Stack />
index.tsx — Main list
search.tsx — Search view
i/[id].tsx — Detail page
components/
theme.tsx
list.tsx
utils/
storage.ts
use-search.ts
```
## Layout Files
Every directory can have a `_layout.tsx` file that wraps all routes in that directory:
```tsx
// app/_layout.tsx
import { Stack } from "expo-router/stack";
export default function RootLayout() {
return <Stack />;
}
```
```tsx
// app/(tabs)/_layout.tsx
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
<Icon sf="house.fill" />
</NativeTabs.Trigger>
</NativeTabs>
);
}
```
## Route Settings
Export `unstable_settings` to configure route behavior:
```tsx
export const unstable_settings = {
anchor: "index",
};
```
- `initialRouteName` was renamed to `anchor` in v4
## Not Found Routes
Create a `+not-found.tsx` file to handle unmatched routes:
```tsx
// app/+not-found.tsx
import { Link } from "expo-router";
import { View, Text } from "react-native";
export default function NotFound() {
return (
<View>
<Text>Page not found</Text>
<Link href="/">Go home</Link>
</View>
);
}
```

View File

@@ -0,0 +1,248 @@
# Search
## Header Search Bar
Add a search bar to the stack header with `headerSearchBarOptions`:
```tsx
<Stack.Screen
name="index"
options={{
headerSearchBarOptions: {
placeholder: "Search",
onChangeText: (event) => console.log(event.nativeEvent.text),
},
}}
/>
```
### Options
```tsx
headerSearchBarOptions: {
// Placeholder text
placeholder: "Search items...",
// Auto-capitalize behavior
autoCapitalize: "none",
// Input type
inputType: "text", // "text" | "phone" | "number" | "email"
// Cancel button text (iOS)
cancelButtonText: "Cancel",
// Hide when scrolling (iOS)
hideWhenScrolling: true,
// Hide navigation bar during search (iOS)
hideNavigationBar: true,
// Obscure background during search (iOS)
obscureBackground: true,
// Placement
placement: "automatic", // "automatic" | "inline" | "stacked"
// Callbacks
onChangeText: (event) => {},
onSearchButtonPress: (event) => {},
onCancelButtonPress: (event) => {},
onFocus: () => {},
onBlur: () => {},
}
```
## useSearch Hook
Reusable hook for search state management:
```tsx
import { useEffect, useState } from "react";
import { useNavigation } from "expo-router";
export function useSearch(options: any = {}) {
const [search, setSearch] = useState("");
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
headerShown: true,
headerSearchBarOptions: {
...options,
onChangeText(e: any) {
setSearch(e.nativeEvent.text);
options.onChangeText?.(e);
},
onSearchButtonPress(e: any) {
setSearch(e.nativeEvent.text);
options.onSearchButtonPress?.(e);
},
onCancelButtonPress(e: any) {
setSearch("");
options.onCancelButtonPress?.(e);
},
},
});
}, [options, navigation]);
return search;
}
```
### Usage
```tsx
function SearchScreen() {
const search = useSearch({ placeholder: "Search items..." });
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<FlatList
data={filteredItems}
renderItem={({ item }) => <ItemRow item={item} />}
/>
);
}
```
## Filtering Patterns
### Simple Text Filter
```tsx
const filtered = items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
```
### Multiple Fields
```tsx
const filtered = items.filter(item => {
const query = search.toLowerCase();
return (
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query) ||
item.tags.some(tag => tag.toLowerCase().includes(query))
);
});
```
### Debounced Search
For expensive filtering or API calls:
```tsx
import { useState, useEffect, useMemo } from "react";
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function SearchScreen() {
const search = useSearch();
const debouncedSearch = useDebounce(search, 300);
const filteredItems = useMemo(() =>
items.filter(item =>
item.name.toLowerCase().includes(debouncedSearch.toLowerCase())
),
[debouncedSearch]
);
return <FlatList data={filteredItems} />;
}
```
## Search with Native Tabs
When using NativeTabs with a search role, the search bar integrates with the tab bar:
```tsx
// app/_layout.tsx
<NativeTabs>
<NativeTabs.Trigger name="(home)">
<Label>Home</Label>
<Icon sf="house.fill" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search">
<Label>Search</Label>
</NativeTabs.Trigger>
</NativeTabs>
```
```tsx
// app/(search)/_layout.tsx
<Stack>
<Stack.Screen
name="index"
options={{
headerSearchBarOptions: {
placeholder: "Search...",
onChangeText: (e) => setSearch(e.nativeEvent.text),
},
}}
/>
</Stack>
```
## Empty States
Show appropriate UI when search returns no results:
```tsx
function SearchResults({ search, items }) {
const filtered = items.filter(/* ... */);
if (search && filtered.length === 0) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={{ color: PlatformColor("secondaryLabel") }}>
No results for "{search}"
</Text>
</View>
);
}
return <FlatList data={filtered} />;
}
```
## Search Suggestions
Show recent searches or suggestions:
```tsx
function SearchScreen() {
const search = useSearch();
const [recentSearches, setRecentSearches] = useState<string[]>([]);
if (!search && recentSearches.length > 0) {
return (
<View>
<Text style={{ color: PlatformColor("secondaryLabel") }}>
Recent Searches
</Text>
{recentSearches.map((term) => (
<Pressable key={term} onPress={() => /* apply search */}>
<Text>{term}</Text>
</Pressable>
))}
</View>
);
}
return <SearchResults search={search} />;
}
```

View File

@@ -0,0 +1,121 @@
# Storage
## Key-Value Storage
Use the localStorage polyfill for key-value storage. **Never use AsyncStorage**
```tsx
import "expo-sqlite/localStorage/install";
// Simple get/set
localStorage.setItem("key", "value");
localStorage.getItem("key");
// Store objects as JSON
localStorage.setItem("user", JSON.stringify({ name: "John", id: 1 }));
const user = JSON.parse(localStorage.getItem("user") ?? "{}");
```
## When to Use What
| Use Case | Solution |
| ---------------------------------------------------- | ----------------------- |
| Simple key-value (settings, preferences, small data) | `localStorage` polyfill |
| Large datasets, complex queries, relational data | Full `expo-sqlite` |
| Sensitive data (tokens, passwords) | `expo-secure-store` |
## Storage with React State
Create a storage utility with subscriptions for reactive updates:
```tsx
// utils/storage.ts
import "expo-sqlite/localStorage/install";
type Listener = () => void;
const listeners = new Map<string, Set<Listener>>();
export const storage = {
get<T>(key: string, defaultValue: T): T {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
},
set<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
listeners.get(key)?.forEach((fn) => fn());
},
subscribe(key: string, listener: Listener): () => void {
if (!listeners.has(key)) listeners.set(key, new Set());
listeners.get(key)!.add(listener);
return () => listeners.get(key)?.delete(listener);
},
};
```
## React Hook for Storage
```tsx
// hooks/use-storage.ts
import { useSyncExternalStore } from "react";
import { storage } from "@/utils/storage";
export function useStorage<T>(
key: string,
defaultValue: T
): [T, (value: T) => void] {
const value = useSyncExternalStore(
(cb) => storage.subscribe(key, cb),
() => storage.get(key, defaultValue)
);
return [value, (newValue: T) => storage.set(key, newValue)];
}
```
Usage:
```tsx
function Settings() {
const [theme, setTheme] = useStorage("theme", "light");
return (
<Switch
value={theme === "dark"}
onValueChange={(dark) => setTheme(dark ? "dark" : "light")}
/>
);
}
```
## Full SQLite for Complex Data
For larger datasets or complex queries, use expo-sqlite directly:
```tsx
import * as SQLite from "expo-sqlite";
const db = await SQLite.openDatabaseAsync("app.db");
// Create table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
date TEXT NOT NULL,
location TEXT
)
`);
// Insert
await db.runAsync("INSERT INTO events (title, date) VALUES (?, ?)", [
"Meeting",
"2024-01-15",
]);
// Query
const events = await db.getAllAsync("SELECT * FROM events WHERE date > ?", [
"2024-01-01",
]);
```

View File

@@ -0,0 +1,368 @@
# Native Tabs
Always prefer NativeTabs from 'expo-router/unstable-native-tabs' for the best iOS experience.
**Requires SDK 54+**
## Basic Usage
```tsx
import {
NativeTabs,
Icon,
Label,
Badge,
} from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
<NativeTabs minimizeBehavior="onScrollDown">
<NativeTabs.Trigger name="index">
<Label>Home</Label>
<Icon sf="house.fill" />
<Badge>9+</Badge>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Icon sf="gear" />
<Label>Settings</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search">
<Label>Search</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
```
## Rules
- You must include a trigger for each tab
- The `NativeTabs.Trigger` 'name' must match the route name, including parentheses (e.g. `<NativeTabs.Trigger name="(search)">`)
- Prefer search tab to be last in the list so it can combine with the search bar
- Use the 'role' prop for common tab types
## Platform Features
Native Tabs use platform-specific tab bar implementations:
- **iOS 26+**: Liquid glass effects with system-native appearance
- **Android**: Material 3 bottom navigation
- Better performance and native feel
## Icon Component
```tsx
// SF Symbol only (iOS)
<Icon sf="house.fill" />
// With Android drawable
<Icon sf="house.fill" drawable="ic_home" />
// Custom image source
<Icon src={require('./icon.png')} />
// State variants (default/selected)
<Icon sf={{ default: "house", selected: "house.fill" }} />
```
## Label Component
```tsx
// Basic label
<Label>Home</Label>
// Hidden label (icon only)
<Label hidden>Home</Label>
```
## Badge Component
```tsx
// Numeric badge
<Badge>9+</Badge>
// Dot indicator (empty badge)
<Badge />
```
## iOS 26 Features
### Liquid Glass Tab Bar
The tab bar automatically adopts liquid glass appearance on iOS 26+.
### Minimize on Scroll
```tsx
<NativeTabs minimizeBehavior="onScrollDown">
```
### Search Tab
Add a dedicated search tab that integrates with the tab bar search field:
```tsx
<NativeTabs.Trigger name="(search)" role="search">
<Label>Search</Label>
</NativeTabs.Trigger>
```
**Note**: Place search tab last for best UX.
### Role Prop
Use semantic roles for special tab types:
```tsx
<NativeTabs.Trigger name="search" role="search" />
<NativeTabs.Trigger name="favorites" role="favorites" />
<NativeTabs.Trigger name="more" role="more" />
```
Available roles: `search` | `more` | `favorites` | `bookmarks` | `contacts` | `downloads` | `featured` | `history` | `mostRecent` | `mostViewed` | `recents` | `topRated`
## Customization
### Tint Color
```tsx
<NativeTabs tintColor="#007AFF">
```
### Dynamic Colors (iOS)
Use DynamicColorIOS for colors that adapt to liquid glass:
```tsx
import { DynamicColorIOS, Platform } from 'react-native';
const adaptiveBlue = Platform.select({
ios: DynamicColorIOS({ light: '#007AFF', dark: '#0A84FF' }),
default: '#007AFF',
});
<NativeTabs tintColor={adaptiveBlue}>
```
## Conditional Tabs
Hide tabs conditionally:
```tsx
<NativeTabs.Trigger name="admin" hidden={!isAdmin}>
<Label>Admin</Label>
<Icon sf="shield.fill" />
</NativeTabs.Trigger>
```
## Behavior Options
```tsx
<NativeTabs.Trigger
name="home"
disablePopToTop // Don't pop stack when tapping active tab
disableScrollToTop // Don't scroll to top when tapping active tab
>
```
## Using Vector Icons
If you must use @expo/vector-icons instead of SF Symbols:
```tsx
import { VectorIcon } from "expo-router/unstable-native-tabs";
import Ionicons from "@expo/vector-icons/Ionicons";
<NativeTabs.Trigger name="home">
<VectorIcon vector={Ionicons} name="home" />
<Label>Home</Label>
</NativeTabs.Trigger>;
```
**Prefer SF Symbols over vector icons for native feel on Apple platforms.**
## Structure with Stacks
Native tabs don't render headers. Nest Stacks inside each tab for navigation headers:
```tsx
// app/(tabs)/_layout.tsx
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="(home)">
<Label>Home</Label>
<Icon sf="house.fill" />
</NativeTabs.Trigger>
</NativeTabs>
);
}
// app/(tabs)/(home)/_layout.tsx
import Stack from "expo-router/stack";
export default function HomeStack() {
return (
<Stack>
<Stack.Screen
name="index"
options={{ title: "Home", headerLargeTitle: true }}
/>
<Stack.Screen name="details" options={{ title: "Details" }} />
</Stack>
);
}
```
## Migration from JS Tabs
### Before (JS Tabs)
```tsx
import { Tabs } from "expo-router";
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<IconSymbol name="house.fill" color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color }) => <IconSymbol name="gear" color={color} />,
}}
/>
</Tabs>
);
}
```
### After (Native Tabs)
```tsx
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
<Icon sf="house.fill" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Label>Settings</Label>
<Icon sf="gear" />
</NativeTabs.Trigger>
</NativeTabs>
);
}
```
### Key Differences
| JS Tabs | Native Tabs |
| -------------------------- | ------------------------- |
| `<Tabs.Screen>` | `<NativeTabs.Trigger>` |
| `options={{ title }}` | `<Label>Title</Label>` |
| `options={{ tabBarIcon }}` | `<Icon sf="symbol" />` |
| Props-based API | React component-based API |
| `tabBarBadge` option | `<Badge>` component |
### Migration Steps
1. **Change imports**
```tsx
// Remove
import { Tabs } from "expo-router";
// Add
import {
NativeTabs,
Icon,
Label,
Badge,
} from "expo-router/unstable-native-tabs";
```
2. **Replace Tabs with NativeTabs**
```tsx
// Before
<Tabs screenOptions={{ ... }}>
// After
<NativeTabs>
```
3. **Convert each Screen to Trigger**
```tsx
// Before
<Tabs.Screen
name="home"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <Icon name="house" color={color} />,
tabBarBadge: 3,
}}
/>
// After
<NativeTabs.Trigger name="home">
<Label>Home</Label>
<Icon sf="house.fill" />
<Badge>3</Badge>
</NativeTabs.Trigger>
```
4. **Move headers to nested Stack** - Native tabs don't render headers
```
app/
(tabs)/
_layout.tsx <- NativeTabs
(home)/
_layout.tsx <- Stack with headers
index.tsx
(settings)/
_layout.tsx <- Stack with headers
index.tsx
```
## Limitations
- **Android**: Maximum 5 tabs (Material Design constraint)
- **Nesting**: Native tabs cannot nest inside other native tabs
- **Tab bar height**: Cannot be measured programmatically
- **FlatList transparency**: Use `disableTransparentOnScrollEdge` to fix issues
## Keyboard Handling (Android)
Configure in app.json:
```json
{
"expo": {
"android": {
"softwareKeyboardLayoutMode": "resize"
}
}
}
```
## Common Issues
1. **Icons not showing on Android**: Add `drawable` prop or use `VectorIcon`
2. **Headers missing**: Nest a Stack inside each tab group
3. **Trigger name mismatch**: Ensure `name` matches exact route name including parentheses
4. **Badge not visible**: Badge must be a child of Trigger, not a prop

View File

@@ -0,0 +1,284 @@
# Toolbars and headers
Add native iOS toolbar items to Stack screens. Items can be placed in the header (left/right) or in a bottom toolbar area.
**Important:** iOS only. Available in Expo SDK 55+.
## Notes app example
```tsx
import { Stack } from "expo-router";
import { ScrollView } from "react-native";
export default function FoldersScreen() {
return (
<>
{/* ScrollView must be the first child of the screen */}
<ScrollView
style={{ flex: 1 }}
contentInsetAdjustmentBehavior="automatic"
>
{/* Screen content */}
</ScrollView>
<Stack.Screen.Title large>Folders</Stack.Screen.Title>
<Stack.SearchBar placeholder="Search" onChangeText={() => {}} />
{/* Header toolbar - right side */}
<Stack.Toolbar placement="right">
<Stack.Toolbar.Button icon="folder.badge.plus" onPress={() => {}} />
<Stack.Toolbar.Button onPress={() => {}}>Edit</Stack.Toolbar.Button>
</Stack.Toolbar>
{/* Bottom toolbar */}
<Stack.Toolbar placement="bottom">
<Stack.Toolbar.SearchBarSlot />
<Stack.Toolbar.Button
icon="square.and.pencil"
onPress={() => {}}
separateBackground
/>
</Stack.Toolbar>
</>
);
}
```
## Mail inbox example
```tsx
import { Color, Stack } from "expo-router";
import { useState } from "react";
import { ScrollView, Text, View } from "react-native";
export default function InboxScreen() {
const [isFilterOpen, setIsFilterOpen] = useState(false);
return (
<>
<ScrollView
style={{ flex: 1 }}
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{ paddingHorizontal: 16 }}
>
{/* Screen content */}
</ScrollView>
<Stack.Screen options={{ headerTransparent: true }} />
<Stack.Screen.Title>Inbox</Stack.Screen.Title>
<Stack.SearchBar placeholder="Search" onChangeText={() => {}} />
{/* Header toolbar - right side */}
<Stack.Toolbar placement="right">
<Stack.Toolbar.Button onPress={() => {}}>Select</Stack.Toolbar.Button>
<Stack.Toolbar.Menu icon="ellipsis">
<Stack.Toolbar.Menu inline>
<Stack.Toolbar.Menu inline title="Sort By">
<Stack.Toolbar.MenuAction isOn>
Categories
</Stack.Toolbar.MenuAction>
<Stack.Toolbar.MenuAction>List</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
<Stack.Toolbar.MenuAction icon="info.circle">
About categories
</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
<Stack.Toolbar.MenuAction icon="person.circle">
Show Contact Photos
</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
</Stack.Toolbar>
{/* Bottom toolbar */}
<Stack.Toolbar placement="bottom">
<Stack.Toolbar.Button
icon="line.3.horizontal.decrease"
selected={isFilterOpen}
onPress={() => setIsFilterOpen((prev) => !prev)}
/>
<Stack.Toolbar.View hidden={!isFilterOpen}>
<View style={{ width: 70, height: 32, justifyContent: "center" }}>
<Text style={{ fontSize: 12, fontWeight: 700 }}>Filter by</Text>
<Text
style={{
fontSize: 12,
fontWeight: 700,
color: Color.ios.systemBlue,
}}
>
Unread
</Text>
</View>
</Stack.Toolbar.View>
<Stack.Toolbar.Spacer />
<Stack.Toolbar.SearchBarSlot />
<Stack.Toolbar.Button
icon="square.and.pencil"
onPress={() => {}}
separateBackground
/>
</Stack.Toolbar>
</>
);
}
```
## Placement
- `"left"` - Header left
- `"right"` - Header right
- `"bottom"` (default) - Bottom toolbar
## Components
### Button
- Icon button: `<Stack.Toolbar.Button icon="star.fill" onPress={() => {}} />`
- Text button: `<Stack.Toolbar.Button onPress={() => {}}>Done</Stack.Toolbar.Button>`
**Props:** `icon`, `image`, `onPress`, `disabled`, `hidden`, `variant` (`"plain"` | `"done"` | `"prominent"`), `tintColor`
### Menu
Dropdown menu for grouping actions.
```tsx
<Stack.Toolbar.Menu icon="ellipsis">
<Stack.Toolbar.Menu inline>
<Stack.Toolbar.MenuAction>Sort by Recently Added</Stack.Toolbar.MenuAction>
<Stack.Toolbar.MenuAction isOn>
Sort by Date Captured
</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
<Stack.Toolbar.Menu title="Filter">
<Stack.Toolbar.Menu inline>
<Stack.Toolbar.MenuAction isOn icon="square.grid.2x2">
All Items
</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
<Stack.Toolbar.MenuAction icon="heart">Favorites</Stack.Toolbar.MenuAction>
<Stack.Toolbar.MenuAction icon="photo">Photos</Stack.Toolbar.MenuAction>
<Stack.Toolbar.MenuAction icon="video">Videos</Stack.Toolbar.MenuAction>
</Stack.Toolbar.Menu>
</Stack.Toolbar.Menu>
```
**Menu Props:** All Button props plus `title`, `inline`, `palette`, `elementSize` (`"small"` | `"medium"` | `"large"`)
**MenuAction Props:** `icon`, `onPress`, `isOn`, `destructive`, `disabled`, `subtitle`
When creating a palette with dividers, use `inline` combined with `elementSize="small"`. `palette` will not apply dividers on iOS 26.
### Spacer
```tsx
<Stack.Toolbar.Spacer /> // Bottom toolbar - flexible
<Stack.Toolbar.Spacer width={16} /> // Header - requires explicit width
```
### View
Embed custom React Native components. When adding a custom view make sure that there is only a single child with **explicit width and height**.
```tsx
<Stack.Toolbar.View>
<View style={{ width: 70, height: 32, justifyContent: "center" }}>
<Text style={{ fontSize: 12, fontWeight: 700 }}>Filter by</Text>
</View>
</Stack.Toolbar.View>
```
You can pass custom components to views as well:
```tsx
function CustomFilterView() {
return (
<View style={{ width: 70, height: 32, justifyContent: "center" }}>
<Text style={{ fontSize: 12, fontWeight: 700 }}>Filter by</Text>
</View>
);
}
...
<Stack.Toolbar.View>
<CustomFilterView />
</Stack.Toolbar.View>
```
## Recommendations
- When creating more complex headers, extract them to a single component
```tsx
export default function Page() {
return (
<>
<ScrollView>{/* Screen content */}</ScrollView>
<InboxHeader />
</>
);
}
function InboxHeader() {
return (
<>
<Stack.Screen.Title>Inbox</Stack.Screen.Title>
<Stack.SearchBar placeholder="Search" onChangeText={() => {}} />
<Stack.Toolbar placement="right">{/* Toolbar buttons */}</Stack.Toolbar>
</>
);
}
```
- When using `Stack.Toolbar`, make sure that all `Stack.Toolbar.*` components are wrapped inside `Stack.Toolbar` component.
This will **not work**:
```tsx
function Buttons() {
return (
<>
<Stack.Toolbar.Button icon="star.fill" onPress={() => {}} />
<Stack.Toolbar.Button onPress={() => {}}>Done</Stack.Toolbar.Button>
</>
);
}
function Page() {
return (
<>
<ScrollView>{/* Screen content */}</ScrollView>
<Stack.Toolbar placement="right">
<Buttons /> {/* ❌ This will NOT work */}
</Stack.Toolbar>
</>
);
}
```
This will work:
```tsx
function ToolbarWithButtons() {
return (
<Stack.Toolbar>
<Stack.Toolbar.Button icon="star.fill" onPress={() => {}} />
<Stack.Toolbar.Button onPress={() => {}}>Done</Stack.Toolbar.Button>
</Stack.Toolbar>
);
}
function Page() {
return (
<>
<ScrollView>{/* Screen content */}</ScrollView>
<ToolbarWithButtons /> {/* ✅ This will work */}
</>
);
}
```
## Limitations
- iOS only
- `placement="bottom"` can only be used inside screen components (not in layout files)
- `Stack.Toolbar.Badge` only works with `placement="left"` or `"right"`
- Header Spacers require explicit `width`
## Reference
Docs https://docs.expo.dev/versions/unversioned/sdk/router - read to see the full API.

View File

@@ -0,0 +1,197 @@
# Visual Effects
## Backdrop Blur
Use `expo-blur` for blur effects. Prefer systemMaterial tints as they adapt to dark mode.
```tsx
import { BlurView } from "expo-blur";
<BlurView tint="systemMaterial" intensity={100} />;
```
### Tint Options
```tsx
// System materials (adapt to dark mode)
<BlurView tint="systemMaterial" />
<BlurView tint="systemThinMaterial" />
<BlurView tint="systemUltraThinMaterial" />
<BlurView tint="systemThickMaterial" />
<BlurView tint="systemChromeMaterial" />
// Basic tints
<BlurView tint="light" />
<BlurView tint="dark" />
<BlurView tint="default" />
// Prominent (more visible)
<BlurView tint="prominent" />
// Extra light/dark
<BlurView tint="extraLight" />
```
### Intensity
Control blur strength with `intensity` (0-100):
```tsx
<BlurView tint="systemMaterial" intensity={50} /> // Subtle
<BlurView tint="systemMaterial" intensity={100} /> // Full
```
### Rounded Corners
BlurView requires `overflow: 'hidden'` to clip rounded corners:
```tsx
<BlurView
tint="systemMaterial"
intensity={100}
style={{
borderRadius: 16,
overflow: 'hidden',
}}
/>
```
### Overlay Pattern
Common pattern for overlaying blur on content:
```tsx
<View style={{ position: 'relative' }}>
<Image source={{ uri: '...' }} style={{ width: '100%', height: 200 }} />
<BlurView
tint="systemUltraThinMaterial"
intensity={80}
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 16,
}}
>
<Text style={{ color: 'white' }}>Caption</Text>
</BlurView>
</View>
```
## Glass Effects (iOS 26+)
Use `expo-glass-effect` for liquid glass backdrops on iOS 26+.
```tsx
import { GlassView } from "expo-glass-effect";
<GlassView style={{ borderRadius: 16, padding: 16 }}>
<Text>Content inside glass</Text>
</GlassView>
```
### Interactive Glass
Add `isInteractive` for buttons and pressable glass:
```tsx
import { GlassView } from "expo-glass-effect";
import { SymbolView } from "expo-symbols";
import { PlatformColor } from "react-native";
<GlassView isInteractive style={{ borderRadius: 50 }}>
<Pressable style={{ padding: 12 }} onPress={handlePress}>
<SymbolView name="plus" tintColor={PlatformColor("label")} size={36} />
</Pressable>
</GlassView>
```
### Glass Buttons
Create liquid glass buttons:
```tsx
function GlassButton({ icon, onPress }) {
return (
<GlassView isInteractive style={{ borderRadius: 50 }}>
<Pressable style={{ padding: 12 }} onPress={onPress}>
<SymbolView name={icon} tintColor={PlatformColor("label")} size={24} />
</Pressable>
</GlassView>
);
}
// Usage
<GlassButton icon="plus" onPress={handleAdd} />
<GlassButton icon="gear" onPress={handleSettings} />
```
### Glass Card
```tsx
<GlassView style={{ borderRadius: 20, padding: 20 }}>
<Text style={{ fontSize: 18, fontWeight: '600', color: PlatformColor("label") }}>
Card Title
</Text>
<Text style={{ color: PlatformColor("secondaryLabel"), marginTop: 8 }}>
Card content goes here
</Text>
</GlassView>
```
### Checking Availability
```tsx
import { isLiquidGlassAvailable } from "expo-glass-effect";
if (isLiquidGlassAvailable()) {
// Use GlassView
} else {
// Fallback to BlurView or solid background
}
```
### Fallback Pattern
```tsx
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import { BlurView } from "expo-blur";
function AdaptiveGlass({ children, style }) {
if (isLiquidGlassAvailable()) {
return <GlassView style={style}>{children}</GlassView>;
}
return (
<BlurView tint="systemMaterial" intensity={80} style={style}>
{children}
</BlurView>
);
}
```
## Sheet with Glass Background
Make sheet backgrounds liquid glass on iOS 26+:
```tsx
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>
```
## Best Practices
- Use `systemMaterial` tints for automatic dark mode support
- Always set `overflow: 'hidden'` on BlurView for rounded corners
- Use `isInteractive` on GlassView for buttons and pressables
- Check `isLiquidGlassAvailable()` and provide fallbacks
- Avoid nesting blur views (performance impact)
- Keep blur intensity reasonable (50-100) for readability

View File

@@ -0,0 +1,605 @@
# WebGPU & Three.js for Expo
**Use this skill for ANY 3D graphics, games, GPU compute, or Three.js features in React Native.**
## Locked Versions (Tested & Working)
```json
{
"react-native-wgpu": "^0.4.1",
"three": "0.172.0",
"@react-three/fiber": "^9.4.0",
"wgpu-matrix": "^3.0.2",
"@types/three": "0.172.0"
}
```
**Critical:** These versions are tested together. Mismatched versions cause type errors and runtime issues.
## Installation
```bash
npm install react-native-wgpu@^0.4.1 three@0.172.0 @react-three/fiber@^9.4.0 wgpu-matrix@^3.0.2 @types/three@0.172.0 --legacy-peer-deps
```
**Note:** `--legacy-peer-deps` may be required due to peer dependency conflicts with canary Expo versions.
## Metro Configuration
Create `metro.config.js` in project root:
```js
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.resolver.resolveRequest = (context, moduleName, platform) => {
// Force 'three' to webgpu build
if (moduleName.startsWith("three")) {
moduleName = "three/webgpu";
}
// Use standard react-three/fiber instead of React Native version
if (platform !== "web" && moduleName.startsWith("@react-three/fiber")) {
return context.resolveRequest(
{
...context,
unstable_conditionNames: ["module"],
mainFields: ["module"],
},
moduleName,
platform
);
}
return context.resolveRequest(context, moduleName, platform);
};
module.exports = config;
```
## Required Lib Files
Create these files in `src/lib/`:
### 1. make-webgpu-renderer.ts
```ts
import type { NativeCanvas } from "react-native-wgpu";
import * as THREE from "three/webgpu";
export class ReactNativeCanvas {
constructor(private canvas: NativeCanvas) {}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
set width(width: number) {
this.canvas.width = width;
}
set height(height: number) {
this.canvas.height = height;
}
get clientWidth() {
return this.canvas.width;
}
get clientHeight() {
return this.canvas.height;
}
set clientWidth(width: number) {
this.canvas.width = width;
}
set clientHeight(height: number) {
this.canvas.height = height;
}
addEventListener(_type: string, _listener: EventListener) {}
removeEventListener(_type: string, _listener: EventListener) {}
dispatchEvent(_event: Event) {}
setPointerCapture() {}
releasePointerCapture() {}
}
export const makeWebGPURenderer = (
context: GPUCanvasContext,
{ antialias = true }: { antialias?: boolean } = {}
) =>
new THREE.WebGPURenderer({
antialias,
// @ts-expect-error
canvas: new ReactNativeCanvas(context.canvas),
context,
});
```
### 2. fiber-canvas.tsx
```tsx
import * as THREE from "three/webgpu";
import React, { useEffect, useRef } from "react";
import type { ReconcilerRoot, RootState } from "@react-three/fiber";
import {
extend,
createRoot,
unmountComponentAtNode,
events,
} from "@react-three/fiber";
import type { ViewProps } from "react-native";
import { PixelRatio } from "react-native";
import { Canvas, type CanvasRef } from "react-native-wgpu";
import {
makeWebGPURenderer,
ReactNativeCanvas,
} from "@/lib/make-webgpu-renderer";
// Extend THREE namespace for R3F - add all components you use
extend({
AmbientLight: THREE.AmbientLight,
DirectionalLight: THREE.DirectionalLight,
PointLight: THREE.PointLight,
SpotLight: THREE.SpotLight,
Mesh: THREE.Mesh,
Group: THREE.Group,
Points: THREE.Points,
BoxGeometry: THREE.BoxGeometry,
SphereGeometry: THREE.SphereGeometry,
CylinderGeometry: THREE.CylinderGeometry,
ConeGeometry: THREE.ConeGeometry,
DodecahedronGeometry: THREE.DodecahedronGeometry,
BufferGeometry: THREE.BufferGeometry,
BufferAttribute: THREE.BufferAttribute,
MeshStandardMaterial: THREE.MeshStandardMaterial,
MeshBasicMaterial: THREE.MeshBasicMaterial,
PointsMaterial: THREE.PointsMaterial,
PerspectiveCamera: THREE.PerspectiveCamera,
Scene: THREE.Scene,
});
interface FiberCanvasProps {
children: React.ReactNode;
style?: ViewProps["style"];
camera?: THREE.PerspectiveCamera;
scene?: THREE.Scene;
}
export const FiberCanvas = ({
children,
style,
scene,
camera,
}: FiberCanvasProps) => {
const root = useRef<ReconcilerRoot<OffscreenCanvas>>(null!);
const canvasRef = useRef<CanvasRef>(null);
useEffect(() => {
const context = canvasRef.current!.getContext("webgpu")!;
const renderer = makeWebGPURenderer(context);
// @ts-expect-error - ReactNativeCanvas wraps native canvas
const canvas = new ReactNativeCanvas(context.canvas) as HTMLCanvasElement;
canvas.width = canvas.clientWidth * PixelRatio.get();
canvas.height = canvas.clientHeight * PixelRatio.get();
const size = {
top: 0,
left: 0,
width: canvas.clientWidth,
height: canvas.clientHeight,
};
if (!root.current) {
root.current = createRoot(canvas);
}
root.current.configure({
size,
events,
scene,
camera,
gl: renderer,
frameloop: "always",
dpr: 1,
onCreated: async (state: RootState) => {
// @ts-expect-error - WebGPU renderer has init method
await state.gl.init();
const renderFrame = state.gl.render.bind(state.gl);
state.gl.render = (s: THREE.Scene, c: THREE.Camera) => {
renderFrame(s, c);
context?.present();
};
},
});
root.current.render(children);
return () => {
if (canvas != null) {
unmountComponentAtNode(canvas!);
}
};
});
return <Canvas ref={canvasRef} style={style} />;
};
```
## Basic 3D Scene
```tsx
import * as THREE from "three/webgpu";
import { View } from "react-native";
import { useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { FiberCanvas } from "@/lib/fiber-canvas";
function RotatingBox() {
const ref = useRef<THREE.Mesh>(null!);
useFrame((_, delta) => {
ref.current.rotation.x += delta;
ref.current.rotation.y += delta * 0.5;
});
return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
function Scene() {
const { camera } = useThree();
useEffect(() => {
camera.position.set(0, 2, 5);
camera.lookAt(0, 0, 0);
}, [camera]);
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} intensity={1} />
<RotatingBox />
</>
);
}
export default function App() {
return (
<View style={{ flex: 1 }}>
<FiberCanvas style={{ flex: 1 }}>
<Scene />
</FiberCanvas>
</View>
);
}
```
## Lazy Loading (Recommended)
Use React.lazy to code-split Three.js for better loading:
```tsx
import React, { Suspense } from "react";
import { ActivityIndicator, View } from "react-native";
const Scene = React.lazy(() => import("@/components/scene"));
export default function Page() {
return (
<View style={{ flex: 1 }}>
<Suspense fallback={<ActivityIndicator size="large" />}>
<Scene />
</Suspense>
</View>
);
}
```
## Common Geometries
```tsx
// Box
<mesh>
<boxGeometry args={[width, height, depth]} />
<meshStandardMaterial color="red" />
</mesh>
// Sphere
<mesh>
<sphereGeometry args={[radius, widthSegments, heightSegments]} />
<meshStandardMaterial color="blue" />
</mesh>
// Cylinder
<mesh>
<cylinderGeometry args={[radiusTop, radiusBottom, height, segments]} />
<meshStandardMaterial color="green" />
</mesh>
// Cone
<mesh>
<coneGeometry args={[radius, height, segments]} />
<meshStandardMaterial color="yellow" />
</mesh>
```
## Lighting
```tsx
// Ambient (uniform light everywhere)
<ambientLight intensity={0.5} />
// Directional (sun-like)
<directionalLight position={[10, 10, 5]} intensity={1} />
// Point (light bulb)
<pointLight position={[0, 5, 0]} intensity={2} distance={10} />
// Spot (flashlight)
<spotLight position={[0, 10, 0]} angle={0.3} penumbra={1} intensity={2} />
```
## Animation with useFrame
```tsx
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import * as THREE from "three/webgpu";
function AnimatedMesh() {
const ref = useRef<THREE.Mesh>(null!);
// Runs every frame - delta is time since last frame
useFrame((state, delta) => {
// Rotate
ref.current.rotation.y += delta;
// Oscillate position
ref.current.position.y = Math.sin(state.clock.elapsedTime) * 2;
});
return (
<mesh ref={ref}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
);
}
```
## Particle Systems
```tsx
import * as THREE from "three/webgpu";
import { useRef, useEffect } from "react";
import { useFrame } from "@react-three/fiber";
function Particles({ count = 500 }) {
const ref = useRef<THREE.Points>(null!);
const positions = useRef<Float32Array>(new Float32Array(count * 3));
useEffect(() => {
for (let i = 0; i < count; i++) {
positions.current[i * 3] = (Math.random() - 0.5) * 50;
positions.current[i * 3 + 1] = (Math.random() - 0.5) * 50;
positions.current[i * 3 + 2] = (Math.random() - 0.5) * 50;
}
}, [count]);
useFrame((_, delta) => {
// Animate particles
for (let i = 0; i < count; i++) {
positions.current[i * 3 + 1] -= delta * 2;
if (positions.current[i * 3 + 1] < -25) {
positions.current[i * 3 + 1] = 25;
}
}
ref.current.geometry.attributes.position.needsUpdate = true;
});
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions.current, 3]}
/>
</bufferGeometry>
<pointsMaterial color="#ffffff" size={0.2} sizeAttenuation />
</points>
);
}
```
## Touch Controls (Orbit)
See the full `orbit-controls.tsx` implementation in the lib files. Usage:
```tsx
import { View } from "react-native";
import { FiberCanvas } from "@/lib/fiber-canvas";
import useControls from "@/lib/orbit-controls";
function Scene() {
const [OrbitControls, events] = useControls();
return (
<View style={{ flex: 1 }} {...events}>
<FiberCanvas style={{ flex: 1 }}>
<OrbitControls />
{/* Your 3D content */}
</FiberCanvas>
</View>
);
}
```
## Common Issues & Solutions
### 1. "X is not part of the THREE namespace"
**Problem:** Error like `AmbientLight is not part of the THREE namespace`
**Solution:** Add the missing component to the `extend()` call in fiber-canvas.tsx:
```tsx
extend({
AmbientLight: THREE.AmbientLight,
// Add other missing components...
});
```
### 2. TypeScript Errors with Three.js
**Problem:** Type mismatches between three.js and R3F
**Solution:** Use `@ts-expect-error` comments where needed:
```tsx
// @ts-expect-error - WebGPU renderer types don't match
await state.gl.init();
```
### 3. Blank Screen
**Problem:** Canvas renders but nothing visible
**Solution:**
1. Ensure camera is positioned correctly and looking at scene
2. Add lighting (objects are black without light)
3. Check that `extend()` includes all components used
### 4. Performance Issues
**Problem:** Low frame rate or stuttering
**Solution:**
- Reduce polygon count in geometries
- Use `useMemo` for static data
- Limit particle count
- Use `instancedMesh` for many identical objects
### 5. Peer Dependency Errors
**Problem:** npm install fails with ERESOLVE
**Solution:** Use `--legacy-peer-deps`:
```bash
npm install <packages> --legacy-peer-deps
```
## Building
WebGPU requires a custom build:
```bash
npx expo prebuild
npx expo run:ios
```
**Note:** WebGPU does NOT work in Expo Go.
## File Structure
```
src/
├── app/
│ └── index.tsx # Entry point with lazy loading
├── components/
│ ├── scene.tsx # Main 3D scene
│ └── game.tsx # Game logic
└── lib/
├── fiber-canvas.tsx # R3F canvas wrapper
├── make-webgpu-renderer.ts # WebGPU renderer
└── orbit-controls.tsx # Touch controls
```
## Decision Tree
```
Need 3D graphics?
├── Simple shapes → mesh + geometry + material
├── Animated objects → useFrame + refs
├── Many objects → instancedMesh
├── Particles → Points + BufferGeometry
Need interaction?
├── Orbit camera → useControls hook
├── Touch objects → onClick on mesh
├── Gestures → react-native-gesture-handler
Performance critical?
├── Static geometry → useMemo
├── Many instances → InstancedMesh
└── Complex scenes → LOD (Level of Detail)
```
## Example: Complete Game Scene
```tsx
import * as THREE from "three/webgpu";
import { View, Text, Pressable } from "react-native";
import { useRef, useState, useCallback } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { FiberCanvas } from "@/lib/fiber-canvas";
function Player({ position }: { position: THREE.Vector3 }) {
const ref = useRef<THREE.Mesh>(null!);
useFrame(() => {
ref.current.position.copy(position);
});
return (
<mesh ref={ref}>
<coneGeometry args={[0.5, 1, 8]} />
<meshStandardMaterial color="#00ffff" />
</mesh>
);
}
function GameScene({ playerX }: { playerX: number }) {
const { camera } = useThree();
const playerPos = useRef(new THREE.Vector3(0, 0, 0));
playerPos.current.x = playerX;
useEffect(() => {
camera.position.set(0, 10, 15);
camera.lookAt(0, 0, 0);
}, [camera]);
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 10, 5]} />
<Player position={playerPos.current} />
</>
);
}
export default function Game() {
const [playerX, setPlayerX] = useState(0);
return (
<View style={{ flex: 1, backgroundColor: "#000" }}>
<FiberCanvas style={{ flex: 1 }}>
<GameScene playerX={playerX} />
</FiberCanvas>
<View style={{ position: "absolute", bottom: 40, flexDirection: "row" }}>
<Pressable onPress={() => setPlayerX((x) => x - 1)}>
<Text style={{ color: "#fff", fontSize: 32 }}></Text>
</Pressable>
<Pressable onPress={() => setPlayerX((x) => x + 1)}>
<Text style={{ color: "#fff", fontSize: 32 }}></Text>
</Pressable>
</View>
</View>
);
}
```

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/anthropics/skills/tree/main/skills/canvas-design",
"type": "github-subdir",
"installed_at": "2026-01-30T02:16:52.673405615Z",
"repo_url": "https://github.com/anthropics/skills.git",
"subdir": "skills/canvas-design",
"version": "69c0b1a"
}

202
canvas-design/LICENSE.txt Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

130
canvas-design/SKILL.md Normal file
View File

@@ -0,0 +1,130 @@
---
name: canvas-design
description: Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
license: Complete terms in LICENSE.txt
---
These are instructions for creating design philosophies - aesthetic movements that are then EXPRESSED VISUALLY. Output only .md files, .pdf files, and .png files.
Complete this in two steps:
1. Design Philosophy Creation (.md file)
2. Express by creating it on a canvas (.pdf file or .png file)
First, undertake this task:
## DESIGN PHILOSOPHY CREATION
To begin, create a VISUAL PHILOSOPHY (not layouts or templates) that will be interpreted through:
- Form, space, color, composition
- Images, graphics, shapes, patterns
- Minimal text as visual accent
### THE CRITICAL UNDERSTANDING
- What is received: Some subtle input or instructions by the user that should be taken into account, but used as a foundation; it should not constrain creative freedom.
- What is created: A design philosophy/aesthetic movement.
- What happens next: Then, the same version receives the philosophy and EXPRESSES IT VISUALLY - creating artifacts that are 90% visual design, 10% essential text.
Consider this approach:
- Write a manifesto for an art movement
- The next phase involves making the artwork
The philosophy must emphasize: Visual expression. Spatial communication. Artistic interpretation. Minimal words.
### HOW TO GENERATE A VISUAL PHILOSOPHY
**Name the movement** (1-2 words): "Brutalist Joy" / "Chromatic Silence" / "Metabolist Dreams"
**Articulate the philosophy** (4-6 paragraphs - concise but complete):
To capture the VISUAL essence, express how the philosophy manifests through:
- Space and form
- Color and material
- Scale and rhythm
- Composition and balance
- Visual hierarchy
**CRITICAL GUIDELINES:**
- **Avoid redundancy**: Each design aspect should be mentioned once. Avoid repeating points about color theory, spatial relationships, or typographic principles unless adding new depth.
- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final work should appear as though it took countless hours to create, was labored over with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted," "the product of deep expertise," "painstaking attention," "master-level execution."
- **Leave creative space**: Remain specific about the aesthetic direction, but concise enough that the next Claude has room to make interpretive choices also at a extremely high level of craftmanship.
The philosophy must guide the next version to express ideas VISUALLY, not through text. Information lives in design, not paragraphs.
### PHILOSOPHY EXAMPLES
**"Concrete Poetry"**
Philosophy: Communication through monumental form and bold geometry.
Visual expression: Massive color blocks, sculptural typography (huge single words, tiny labels), Brutalist spatial divisions, Polish poster energy meets Le Corbusier. Ideas expressed through visual weight and spatial tension, not explanation. Text as rare, powerful gesture - never paragraphs, only essential words integrated into the visual architecture. Every element placed with the precision of a master craftsman.
**"Chromatic Language"**
Philosophy: Color as the primary information system.
Visual expression: Geometric precision where color zones create meaning. Typography minimal - small sans-serif labels letting chromatic fields communicate. Think Josef Albers' interaction meets data visualization. Information encoded spatially and chromatically. Words only to anchor what color already shows. The result of painstaking chromatic calibration.
**"Analog Meditation"**
Philosophy: Quiet visual contemplation through texture and breathing room.
Visual expression: Paper grain, ink bleeds, vast negative space. Photography and illustration dominate. Typography whispered (small, restrained, serving the visual). Japanese photobook aesthetic. Images breathe across pages. Text appears sparingly - short phrases, never explanatory blocks. Each composition balanced with the care of a meditation practice.
**"Organic Systems"**
Philosophy: Natural clustering and modular growth patterns.
Visual expression: Rounded forms, organic arrangements, color from nature through architecture. Information shown through visual diagrams, spatial relationships, iconography. Text only for key labels floating in space. The composition tells the story through expert spatial orchestration.
**"Geometric Silence"**
Philosophy: Pure order and restraint.
Visual expression: Grid-based precision, bold photography or stark graphics, dramatic negative space. Typography precise but minimal - small essential text, large quiet zones. Swiss formalism meets Brutalist material honesty. Structure communicates, not words. Every alignment the work of countless refinements.
*These are condensed examples. The actual design philosophy should be 4-6 substantial paragraphs.*
### ESSENTIAL PRINCIPLES
- **VISUAL PHILOSOPHY**: Create an aesthetic worldview to be expressed through design
- **MINIMAL TEXT**: Always emphasize that text is sparse, essential-only, integrated as visual element - never lengthy
- **SPATIAL EXPRESSION**: Ideas communicate through space, form, color, composition - not paragraphs
- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy visually - provide creative room
- **PURE DESIGN**: This is about making ART OBJECTS, not documents with decoration
- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final work must look meticulously crafted, labored over with care, the product of countless hours by someone at the top of their field
**The design philosophy should be 4-6 paragraphs long.** Fill it with poetic design philosophy that brings together the core vision. Avoid repeating the same points. Keep the design philosophy generic without mentioning the intention of the art, as if it can be used wherever. Output the design philosophy as a .md file.
---
## DEDUCING THE SUBTLE REFERENCE
**CRITICAL STEP**: Before creating the canvas, identify the subtle conceptual thread from the original request.
**THE ESSENTIAL PRINCIPLE**:
The topic is a **subtle, niche reference embedded within the art itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful abstract composition. The design philosophy provides the aesthetic language. The deduced topic provides the soul - the quiet conceptual DNA woven invisibly into form, color, and composition.
This is **VERY IMPORTANT**: The reference must be refined so it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song - only those who know will catch it, but everyone appreciates the music.
---
## CANVAS CREATION
With both the philosophy and the conceptual framework established, express it on a canvas. Take a moment to gather thoughts and clear the mind. Use the design philosophy created and the instructions below to craft a masterpiece, embodying all aspects of the philosophy with expert craftsmanship.
**IMPORTANT**: For any type of content, even if the user requests something for a movie/game/book, the approach should still be sophisticated. Never lose sight of the idea that this should be art, not something that's cartoony or amateur.
To create museum or magazine quality work, use the design philosophy as the foundation. Create one single page, highly visual, design-forward PDF or PNG output (unless asked for more pages). Generally use repeating patterns and perfect shapes. Treat the abstract philosophical design as if it were a scientific bible, borrowing the visual language of systematic observation—dense accumulation of marks, repeated elements, or layered patterns that build meaning through patient repetition and reward sustained viewing. Add sparse, clinical typography and systematic reference markers that suggest this could be a diagram from an imaginary discipline, treating the invisible subject with the same reverence typically reserved for documenting observable phenomena. Anchor the piece with simple phrase(s) or details positioned subtly, using a limited color palette that feels intentional and cohesive. Embrace the paradox of using analytical visual language to express ideas about human experience: the result should feel like an artifact that proves something ephemeral can be studied, mapped, and understood through careful attention. This is true art.
**Text as a contextual element**: Text is always minimal and visual-first, but let context guide whether that means whisper-quiet labels or bold typographic gestures. A punk venue poster might have larger, more aggressive type than a minimalist ceramics studio identity. Most of the time, font should be thin. All use of fonts must be design-forward and prioritize visual communication. Regardless of text scale, nothing falls off the page and nothing overlaps. Every element must be contained within the canvas boundaries with proper margins. Check carefully that all text, graphics, and visual elements have breathing room and clear separation. This is non-negotiable for professional execution. **IMPORTANT: Use different fonts if writing text. Search the `./canvas-fonts` directory. Regardless of approach, sophistication is non-negotiable.**
Download and use whatever fonts are needed to make this a reality. Get creative by making the typography actually part of the art itself -- if the art is abstract, bring the font onto the canvas, not typeset digitally.
To push boundaries, follow design instinct/intuition while using the philosophy as a guiding principle. Embrace ultimate design freedom and choice. Push aesthetics and design to the frontier.
**CRITICAL**: To achieve human-crafted quality (not AI-generated), create work that looks like it took countless hours. Make it appear as though someone at the absolute top of their field labored over every detail with painstaking care. Ensure the composition, spacing, color choices, typography - everything screams expert-level craftsmanship. Double-check that nothing overlaps, formatting is flawless, every detail perfect. Create something that could be shown to people to prove expertise and rank as undeniably impressive.
Output the final result as a single, downloadable .pdf or .png file, alongside the design philosophy used as a .md file.
---
## FINAL STEP
**IMPORTANT**: The user ALREADY said "It isn't perfect enough. It must be pristine, a masterpiece if craftsmanship, as if it were about to be displayed in a museum."
**CRITICAL**: To refine the work, avoid adding more graphics; instead refine what has been created and make it extremely crisp, respecting the design philosophy and the principles of minimalism entirely. Rather than adding a fun filter or refactoring a font, consider how to make the existing composition more cohesive with the art. If the instinct is to call a new function or draw a new shape, STOP and instead ask: "How can I make what's already here more of a piece of art?"
Take a second pass. Go back to the code and refine/polish further to make this a philosophically designed masterpiece.
## MULTI-PAGE OPTION
To create additional pages when requested, create more creative pages along the same lines as the design philosophy but distinctly different as well. Bundle those pages in the same .pdf or many .pngs. Treat the first page as just a single page in a whole coffee table book waiting to be filled. Make the next pages unique twists and memories of the original. Have them almost tell a story in a very tasteful way. Exercise full creative freedom.

View File

@@ -0,0 +1,93 @@
Copyright 2012 The Arsenal Project Authors (andrij.design@gmail.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2019 The Big Shoulders Project Authors (https://github.com/xotypeco/big_shoulders)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2024 The Boldonse Project Authors (https://github.com/googlefonts/boldonse)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,94 @@
Copyright (c) 2011 by LatinoType Limitada (luciano@latinotype.com),
with Reserved Font Names "Erica One"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2022 The Gloock Project Authors (https://github.com/duartp/gloock)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More