{
- await env.EMAIL.send({
- from: { name: "Acme Corp", email: "noreply@yourdomain.com" },
- to: [
- { name: "Alice", email: "alice@example.com" },
- "bob@example.com"
- ],
- subject: "Your order #12345 has shipped",
- text: "Track your package at: https://track.example.com/12345",
- html: "Track your package at: View tracking
",
- reply_to: { name: "Support", email: "support@yourdomain.com" }
- });
-
- return new Response("Email sent");
- }
-} satisfies ExportedHandler;
-```
-
-### SendEmail Constraints
-
-- **From address**: Must be on verified domain (your domain with Email Routing enabled)
-- **Volume limits**: Transactional only, no bulk/marketing email
-- **Rate limits**: 100 emails/minute on Free plan, higher on Paid
-- **No attachments**: Use links to hosted files instead
-- **No DKIM control**: Cloudflare signs automatically
-
-## REST API Operations
-
-Base URL: `https://api.cloudflare.com/client/v4`
-
-### Authentication
-
-```bash
-curl -H "Authorization: Bearer $API_TOKEN" https://api.cloudflare.com/client/v4/...
-```
-
-### Key Endpoints
-
-| Operation | Method | Endpoint |
-|-----------|--------|----------|
-| Enable routing | POST | `/zones/{zone_id}/email/routing/enable` |
-| Disable routing | POST | `/zones/{zone_id}/email/routing/disable` |
-| List rules | GET | `/zones/{zone_id}/email/routing/rules` |
-| Create rule | POST | `/zones/{zone_id}/email/routing/rules` |
-| Verify destination | POST | `/zones/{zone_id}/email/routing/addresses` |
-| List destinations | GET | `/zones/{zone_id}/email/routing/addresses` |
-
-### Create Routing Rule Example
-
-```bash
-curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/rules" \
- -H "Authorization: Bearer $API_TOKEN" \
- -H "Content-Type: application/json" \
- -d '{
- "enabled": true,
- "name": "Forward sales",
- "matchers": [{"type": "literal", "field": "to", "value": "sales@yourdomain.com"}],
- "actions": [{"type": "forward", "value": ["alice@company.com"]}],
- "priority": 0
- }'
-```
-
-Matcher types: `literal` (exact match), `all` (catch-all).
diff --git a/skills/cloudflare/references/email-routing/configuration.md b/skills/cloudflare/references/email-routing/configuration.md
deleted file mode 100644
index 3f9613e..0000000
--- a/skills/cloudflare/references/email-routing/configuration.md
+++ /dev/null
@@ -1,186 +0,0 @@
-# Email Routing Configuration
-
-## Wrangler Configuration
-
-### Basic Email Worker
-
-```jsonc
-// wrangler.jsonc
-{
- "name": "email-worker",
- "main": "src/index.ts",
- "compatibility_date": "2025-01-01",
- "send_email": [{ "name": "EMAIL" }]
-}
-```
-
-```typescript
-// src/index.ts
-export default {
- async email(message, env, ctx) {
- await message.forward("destination@example.com");
- }
-} satisfies ExportedHandler;
-```
-
-### With Storage Bindings
-
-```jsonc
-{
- "name": "email-processor",
- "send_email": [{ "name": "EMAIL" }],
- "kv_namespaces": [{ "binding": "KV", "id": "abc123" }],
- "r2_buckets": [{ "binding": "R2", "bucket_name": "emails" }],
- "d1_databases": [{ "binding": "DB", "database_id": "def456" }]
-}
-```
-
-```typescript
-interface Env {
- EMAIL: SendEmail;
- KV: KVNamespace;
- R2: R2Bucket;
- DB: D1Database;
-}
-```
-
-## Local Development
-
-```bash
-npx wrangler dev
-
-# Test with curl
-curl -X POST 'http://localhost:8787/__email' \
- --header 'content-type: message/rfc822' \
- --data 'From: test@example.com
-To: you@yourdomain.com
-Subject: Test
-
-Body'
-```
-
-## Deployment
-
-```bash
-npx wrangler deploy
-```
-
-**Connect to Email Routing:**
-
-Dashboard: Email > Email Routing > [domain] > Settings > Email Workers > Select worker
-
-API:
-```bash
-curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/settings" \
- -H "Authorization: Bearer $API_TOKEN" \
- -d '{"enabled": true, "worker": "email-worker"}'
-```
-
-## DNS (Auto-Created)
-
-```dns
-yourdomain.com. IN MX 1 isaac.mx.cloudflare.net.
-yourdomain.com. IN MX 2 linda.mx.cloudflare.net.
-yourdomain.com. IN MX 3 amir.mx.cloudflare.net.
-yourdomain.com. IN TXT "v=spf1 include:_spf.mx.cloudflare.net ~all"
-```
-
-## Secrets & Variables
-
-```bash
-# Secrets (encrypted)
-npx wrangler secret put API_KEY
-
-# Variables (plain)
-# wrangler.jsonc
-{ "vars": { "THRESHOLD": "5.0" } }
-```
-
-```typescript
-interface Env {
- API_KEY: string;
- THRESHOLD: string;
-}
-```
-
-## TypeScript Setup
-
-```bash
-npm install --save-dev @cloudflare/workers-types
-```
-
-```json
-// tsconfig.json
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ES2022",
- "lib": ["ES2022"],
- "types": ["@cloudflare/workers-types"],
- "moduleResolution": "bundler",
- "strict": true
- }
-}
-```
-
-```typescript
-import type { ForwardableEmailMessage } from "@cloudflare/workers-types";
-
-export default {
- async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise {
- await message.forward("dest@example.com");
- }
-} satisfies ExportedHandler;
-```
-
-## Dependencies
-
-```bash
-npm install postal-mime
-```
-
-```typescript
-import PostalMime from 'postal-mime';
-
-export default {
- async email(message, env, ctx) {
- const parser = new PostalMime();
- const email = await parser.parse(await message.raw.arrayBuffer());
- console.log(email.subject);
- await message.forward("inbox@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## Multi-Environment
-
-```bash
-# wrangler.dev.jsonc
-{ "name": "worker-dev", "vars": { "ENV": "dev" } }
-
-# wrangler.prod.jsonc
-{ "name": "worker-prod", "vars": { "ENV": "prod" } }
-
-npx wrangler deploy --config wrangler.dev.jsonc
-npx wrangler deploy --config wrangler.prod.jsonc
-```
-
-## CI/CD (GitHub Actions)
-
-```yaml
-# .github/workflows/deploy.yml
-name: Deploy
-on:
- push:
- branches: [main]
-jobs:
- deploy:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
- - run: npm ci
- - run: npx wrangler deploy
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
-```
diff --git a/skills/cloudflare/references/email-routing/gotchas.md b/skills/cloudflare/references/email-routing/gotchas.md
deleted file mode 100644
index 71dcbfc..0000000
--- a/skills/cloudflare/references/email-routing/gotchas.md
+++ /dev/null
@@ -1,196 +0,0 @@
-# Gotchas & Troubleshooting
-
-## Critical Pitfalls
-
-### Stream Consumption (MOST COMMON)
-
-**Problem:** "stream already consumed" or worker hangs
-
-**Cause:** `message.raw` is `ReadableStream` - consume once only
-
-**Solution:**
-```typescript
-// ❌ WRONG
-const email1 = await parser.parse(await message.raw.arrayBuffer());
-const email2 = await parser.parse(await message.raw.arrayBuffer()); // FAILS
-
-// ✅ CORRECT
-const raw = await message.raw.arrayBuffer();
-const email = await parser.parse(raw);
-```
-
-Consume `message.raw` immediately before any async operations.
-
-### Destination Verification
-
-**Problem:** Emails not forwarding
-
-**Cause:** Destination unverified
-
-**Solution:** Add destination, check inbox for verification email, click link. Verify status: `GET /zones/{id}/email/routing/addresses`
-
-### Mail Authentication
-
-**Problem:** Legitimate emails rejected
-
-**Cause:** Missing SPF/DKIM/DMARC on sender domain
-
-**Solution:** Configure sender DNS:
-```dns
-example.com. IN TXT "v=spf1 include:_spf.example.com ~all"
-selector._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=..."
-_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine"
-```
-
-### Envelope vs Header
-
-**Problem:** Filtering on wrong address
-
-**Solution:**
-```typescript
-// Routing/auth: envelope
-if (message.from === "trusted@example.com") { }
-
-// Display: headers
-const display = message.headers.get("from");
-```
-
-### SendEmail Limits
-
-| Issue | Limit | Solution |
-|-------|-------|----------|
-| From domain | Must own | Use Email Routing domain |
-| Volume | ~100/min Free | Upgrade or throttle |
-| Attachments | Not supported | Link to R2 |
-| Type | Transactional | No bulk |
-
-## Common Errors
-
-### CPU Time Exceeded
-
-**Cause:** Heavy parsing, large emails
-
-**Solution:**
-```typescript
-const size = parseInt(message.headers.get("content-length") || "0") / 1024 / 1024;
-if (size > 20) {
- message.setReject("Too large");
- return;
-}
-
-ctx.waitUntil(expensiveWork());
-await message.forward("dest@example.com");
-```
-
-### Rule Not Triggering
-
-**Causes:** Priority conflict, matcher error, catch-all override
-
-**Solution:** Check priority (lower=first), verify exact match, confirm destination verified
-
-### Undefined Property
-
-**Cause:** Missing header
-
-**Solution:**
-```typescript
-// ❌ WRONG
-const subj = message.headers.get("subject").toLowerCase();
-
-// ✅ CORRECT
-const subj = message.headers.get("subject")?.toLowerCase() || "";
-```
-
-## Limits
-
-| Resource | Free | Paid |
-|----------|------|------|
-| Email size | 25 MB | 25 MB |
-| Rules | 200 | 200 |
-| Destinations | 200 | 200 |
-| CPU time | 10ms | 30s (default), 5min (max) |
-| SendEmail | ~100/min | Higher |
-
-## Debugging
-
-### Local
-
-```bash
-npx wrangler dev
-
-curl -X POST 'http://localhost:8787/__email' \
- --header 'content-type: message/rfc822' \
- --data 'From: test@example.com
-To: you@yourdomain.com
-Subject: Test
-
-Body'
-```
-
-### Production
-
-```bash
-npx wrangler tail
-```
-
-### Pattern
-
-```typescript
-export default {
- async email(message, env, ctx) {
- try {
- console.log("From:", message.from);
- await process(message, env);
- } catch (err) {
- console.error(err);
- message.setReject(err.message);
- }
- }
-} satisfies ExportedHandler;
-```
-
-## Auth Troubleshooting
-
-### Check Status
-
-```typescript
-const auth = message.headers.get("authentication-results") || "";
-console.log({
- spf: auth.includes("spf=pass"),
- dkim: auth.includes("dkim=pass"),
- dmarc: auth.includes("dmarc=pass")
-});
-
-if (!auth.includes("pass")) {
- message.setReject("Failed auth");
- return;
-}
-```
-
-### SPF Issues
-
-**Causes:** Forwarding breaks SPF, too many lookups (>10), missing includes
-
-**Solution:**
-```dns
-; ✅ Good
-example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
-
-; ❌ Bad - too many
-example.com. IN TXT "v=spf1 include:a.com include:b.com ... ~all"
-```
-
-### DMARC Alignment
-
-**Cause:** From domain must match SPF/DKIM domain
-
-## Best Practices
-
-1. Consume `message.raw` immediately
-2. Verify destinations
-3. Handle missing headers (`?.`)
-4. Use envelope for routing
-5. Check spam scores
-6. Test locally first
-7. Use `ctx.waitUntil` for background work
-8. Size-check early
diff --git a/skills/cloudflare/references/email-routing/patterns.md b/skills/cloudflare/references/email-routing/patterns.md
deleted file mode 100644
index 2163677..0000000
--- a/skills/cloudflare/references/email-routing/patterns.md
+++ /dev/null
@@ -1,229 +0,0 @@
-# Common Patterns
-
-## 1. Allowlist/Blocklist
-
-```typescript
-// Allowlist
-const allowed = ["user@example.com", "trusted@corp.com"];
-if (!allowed.includes(message.from)) {
- message.setReject("Not allowed");
- return;
-}
-await message.forward("inbox@corp.com");
-```
-
-## 2. Parse Email Body
-
-```typescript
-import PostalMime from 'postal-mime';
-
-export default {
- async email(message, env, ctx) {
- // CRITICAL: Consume stream immediately
- const raw = await message.raw.arrayBuffer();
-
- const parser = new PostalMime();
- const email = await parser.parse(raw);
-
- console.log({
- subject: email.subject,
- text: email.text,
- html: email.html,
- from: email.from.address,
- attachments: email.attachments.length
- });
-
- await message.forward("inbox@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## 3. Spam Filter
-
-```typescript
-const score = parseFloat(message.headers.get("x-cf-spamh-score") || "0");
-if (score > 5) {
- message.setReject("Spam detected");
- return;
-}
-await message.forward("inbox@corp.com");
-```
-
-## 4. Archive to R2
-
-```typescript
-interface Env { R2: R2Bucket; }
-
-export default {
- async email(message, env, ctx) {
- const raw = await message.raw.arrayBuffer();
-
- const key = `${new Date().toISOString()}-${message.from}.eml`;
- await env.R2.put(key, raw, {
- httpMetadata: { contentType: "message/rfc822" }
- });
-
- await message.forward("inbox@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## 5. Store Metadata in KV
-
-```typescript
-import PostalMime from 'postal-mime';
-
-interface Env { KV: KVNamespace; }
-
-export default {
- async email(message, env, ctx) {
- const raw = await message.raw.arrayBuffer();
- const parser = new PostalMime();
- const email = await parser.parse(raw);
-
- const metadata = {
- from: email.from.address,
- subject: email.subject,
- timestamp: new Date().toISOString(),
- size: raw.byteLength
- };
-
- await env.KV.put(`email:${Date.now()}`, JSON.stringify(metadata));
- await message.forward("inbox@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## 6. Subject-Based Routing
-
-```typescript
-export default {
- async email(message, env, ctx) {
- const subject = message.headers.get("subject")?.toLowerCase() || "";
-
- if (subject.includes("[urgent]")) {
- await message.forward("oncall@corp.com");
- } else if (subject.includes("[billing]")) {
- await message.forward("billing@corp.com");
- } else if (subject.includes("[support]")) {
- await message.forward("support@corp.com");
- } else {
- await message.forward("general@corp.com");
- }
- }
-} satisfies ExportedHandler;
-```
-
-## 7. Auto-Reply
-
-```typescript
-interface Env {
- EMAIL: SendEmail;
- REPLIED: KVNamespace;
-}
-
-export default {
- async email(message, env, ctx) {
- const msgId = message.headers.get("message-id");
-
- if (msgId && await env.REPLIED.get(msgId)) {
- await message.forward("archive@corp.com");
- return;
- }
-
- ctx.waitUntil((async () => {
- await env.EMAIL.send({
- from: "noreply@yourdomain.com",
- to: message.from,
- subject: "Re: " + (message.headers.get("subject") || ""),
- text: "Thank you. We'll respond within 24h."
- });
- if (msgId) await env.REPLIED.put(msgId, "1", { expirationTtl: 604800 });
- })());
-
- await message.forward("support@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## 8. Extract Attachments
-
-```typescript
-import PostalMime from 'postal-mime';
-
-interface Env { ATTACHMENTS: R2Bucket; }
-
-export default {
- async email(message, env, ctx) {
- const parser = new PostalMime();
- const email = await parser.parse(await message.raw.arrayBuffer());
-
- for (const att of email.attachments) {
- const key = `${Date.now()}-${att.filename}`;
- await env.ATTACHMENTS.put(key, att.content, {
- httpMetadata: { contentType: att.mimeType }
- });
- }
-
- await message.forward("inbox@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## 9. Log to D1
-
-```typescript
-import PostalMime from 'postal-mime';
-
-interface Env { DB: D1Database; }
-
-export default {
- async email(message, env, ctx) {
- const parser = new PostalMime();
- const email = await parser.parse(await message.raw.arrayBuffer());
-
- ctx.waitUntil(
- env.DB.prepare("INSERT INTO log (ts, from_addr, subj) VALUES (?, ?, ?)")
- .bind(new Date().toISOString(), email.from.address, email.subject || "")
- .run()
- );
-
- await message.forward("inbox@corp.com");
- }
-} satisfies ExportedHandler;
-```
-
-## 10. Multi-Tenant
-
-```typescript
-interface Env { TENANTS: KVNamespace; }
-
-export default {
- async email(message, env, ctx) {
- const subdomain = message.to.split("@")[1].split(".")[0];
- const config = await env.TENANTS.get(subdomain, "json") as { forward: string } | null;
-
- if (!config) {
- message.setReject("Unknown tenant");
- return;
- }
-
- await message.forward(config.forward);
- }
-} satisfies ExportedHandler;
-```
-
-## Summary
-
-| Pattern | Use Case | Storage |
-|---------|----------|---------|
-| Allowlist | Security | None |
-| Parse | Body/attachments | None |
-| Spam Filter | Reduce spam | None |
-| R2 Archive | Email storage | R2 |
-| KV Meta | Analytics | KV |
-| Subject Route | Dept routing | None |
-| Auto-Reply | Support | KV |
-| Attachments | Doc mgmt | R2 |
-| D1 Log | Audit trail | D1 |
-| Multi-Tenant | SaaS | KV |
diff --git a/skills/cloudflare/references/email-workers/README.md b/skills/cloudflare/references/email-workers/README.md
deleted file mode 100644
index 8f44197..0000000
--- a/skills/cloudflare/references/email-workers/README.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# Cloudflare Email Workers
-
-Process incoming emails programmatically using Cloudflare Workers runtime.
-
-## Overview
-
-Email Workers enable custom email processing logic at the edge. Build spam filters, auto-responders, ticket systems, notification handlers, and more using the same Workers runtime you use for HTTP requests.
-
-**Key capabilities**:
-- Process inbound emails with full message access
-- Forward to verified destinations
-- Send replies with proper threading
-- Parse MIME content and attachments
-- Integrate with KV, R2, D1, and external APIs
-
-## Quick Start
-
-### Minimal ES Modules Handler
-
-```typescript
-export default {
- async email(message, env, ctx) {
- // Reject spam
- if (message.from.includes('spam.com')) {
- message.setReject('Blocked');
- return;
- }
-
- // Forward to inbox
- await message.forward('inbox@example.com');
- }
-};
-```
-
-### Core Operations
-
-| Operation | Method | Use Case |
-|-----------|--------|----------|
-| Forward | `message.forward(to, headers?)` | Route to verified destination |
-| Reject | `message.setReject(reason)` | Block with SMTP error |
-| Reply | `message.reply(emailMessage)` | Auto-respond with threading |
-| Parse | postal-mime library | Extract subject, body, attachments |
-
-## Reading Order
-
-For comprehensive understanding, read files in this order:
-
-1. **README.md** (this file) - Overview and quick start
-2. **configuration.md** - Setup, deployment, bindings
-3. **api.md** - Complete API reference
-4. **patterns.md** - Real-world implementation examples
-5. **gotchas.md** - Critical pitfalls and debugging
-
-## In This Reference
-
-| File | Description | Key Topics |
-|------|-------------|------------|
-| [api.md](./api.md) | Complete API reference | ForwardableEmailMessage, SendEmail bindings, reply() method, postal-mime/mimetext APIs |
-| [configuration.md](./configuration.md) | Setup and configuration | wrangler.jsonc, bindings, deployment, dependencies |
-| [patterns.md](./patterns.md) | Real-world examples | Allowlists from KV, auto-reply with threading, attachment extraction, webhook notifications |
-| [gotchas.md](./gotchas.md) | Pitfalls and debugging | Stream consumption, ctx.waitUntil errors, security, limits |
-
-## Architecture
-
-```
-Incoming Email → Email Routing → Email Worker
- ↓
- Process + Decide
- ↓
- ┌───────────────┼───────────────┐
- ↓ ↓ ↓
- Forward Reply Reject
-```
-
-**Event flow**:
-1. Email arrives at your domain
-2. Email Routing matches route (e.g., `support@example.com`)
-3. Bound Email Worker receives `ForwardableEmailMessage`
-4. Worker processes and takes action (forward/reply/reject)
-5. Email delivered or rejected based on worker logic
-
-## Key Concepts
-
-### Envelope vs Headers
-
-- **Envelope addresses** (`message.from`, `message.to`): SMTP transport addresses (trusted)
-- **Header addresses** (parsed from body): Display addresses (can be spoofed)
-
-Use envelope addresses for security decisions.
-
-### Single-Use Streams
-
-`message.raw` is a ReadableStream that can only be read once. Buffer to ArrayBuffer for multiple uses.
-
-```typescript
-// Buffer first
-const buffer = await new Response(message.raw).arrayBuffer();
-const email = await PostalMime.parse(buffer);
-```
-
-See [gotchas.md](./gotchas.md#readablestream-can-only-be-consumed-once) for details.
-
-### Verified Destinations
-
-`forward()` only works with addresses verified in the Cloudflare Email Routing dashboard. Add destinations before deployment.
-
-## Use Cases
-
-- **Spam filtering**: Block based on sender, content, or reputation
-- **Auto-responders**: Send acknowledgment replies with threading
-- **Ticket creation**: Parse emails and create support tickets
-- **Email archival**: Store in KV, R2, or D1
-- **Notification routing**: Forward to Slack, Discord, or webhooks
-- **Attachment processing**: Extract files to R2 storage
-- **Multi-tenant routing**: Route based on recipient subdomain
-- **Size filtering**: Reject oversized attachments
-
-## Limits
-
-| Limit | Value |
-|-------|-------|
-| Max message size | 25 MiB |
-| Max routing rules | 200 |
-| Max destinations | 200 |
-| CPU time (free tier) | 10ms |
-| CPU time (paid tier) | 30s (default), 5min (max) |
-
-See [gotchas.md](./gotchas.md#limits-reference) for complete limits table.
-
-## Prerequisites
-
-Before deploying Email Workers:
-
-1. **Enable Email Routing** in Cloudflare dashboard for your domain
-2. **Verify destination addresses** for forwarding
-3. **Configure DMARC/SPF** for sending domains (required for replies)
-4. **Set up wrangler.jsonc** with SendEmail binding
-
-See [configuration.md](./configuration.md) for detailed setup.
-
-## Service Worker Syntax (Deprecated)
-
-Modern projects should use ES modules format shown above. Service Worker syntax (`addEventListener('email', ...)`) is deprecated but still supported.
-
-## See Also
-
-- [Email Routing Documentation](https://developers.cloudflare.com/email-routing/)
-- [Workers Platform](https://developers.cloudflare.com/workers/)
-- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)
-- [postal-mime on npm](https://www.npmjs.com/package/postal-mime)
-- [mimetext on npm](https://www.npmjs.com/package/mimetext)
diff --git a/skills/cloudflare/references/email-workers/api.md b/skills/cloudflare/references/email-workers/api.md
deleted file mode 100644
index 74da66c..0000000
--- a/skills/cloudflare/references/email-workers/api.md
+++ /dev/null
@@ -1,237 +0,0 @@
-# Email Workers API Reference
-
-Complete API reference for Cloudflare Email Workers runtime.
-
-## ForwardableEmailMessage Interface
-
-The main interface passed to email handlers.
-
-```typescript
-interface ForwardableEmailMessage {
- readonly from: string; // Envelope MAIL FROM (SMTP sender)
- readonly to: string; // Envelope RCPT TO (SMTP recipient)
- readonly headers: Headers; // Web-standard Headers object
- readonly raw: ReadableStream; // Raw MIME message (single-use stream)
- readonly rawSize: number; // Total message size in bytes
-
- setReject(reason: string): void;
- forward(rcptTo: string, headers?: Headers): Promise;
- reply(message: EmailMessage): Promise;
-}
-```
-
-### Properties
-
-| Property | Type | Description |
-|----------|------|-------------|
-| `from` | string | Envelope sender (SMTP MAIL FROM) - use for security |
-| `to` | string | Envelope recipient (SMTP RCPT TO) |
-| `headers` | Headers | Message headers (Subject, Message-ID, etc.) |
-| `raw` | ReadableStream | Raw MIME message (**single-use**, buffer first) |
-| `rawSize` | number | Message size in bytes |
-
-### Methods
-
-#### setReject(reason: string): void
-
-Reject with permanent SMTP 5xx error. Email not delivered, sender may receive bounce.
-
-```typescript
-if (blockList.includes(message.from)) {
- message.setReject('Sender blocked');
-}
-```
-
-#### forward(rcptTo: string, headers?: Headers): Promise
-
-Forward to verified destination. Only `X-*` custom headers allowed.
-
-```typescript
-await message.forward('inbox@example.com');
-
-// With custom headers
-const h = new Headers();
-h.set('X-Processed-By', 'worker');
-await message.forward('inbox@example.com', h);
-```
-
-#### reply(message: EmailMessage): Promise
-
-Send a reply to the original sender (March 2025 feature).
-
-```typescript
-import { EmailMessage } from 'cloudflare:email';
-import { createMimeMessage } from 'mimetext';
-
-const msg = createMimeMessage();
-msg.setSender({ name: 'Support', addr: 'support@example.com' });
-msg.setRecipient(message.from);
-msg.setSubject(`Re: ${message.headers.get('Subject')}`);
-msg.setHeader('In-Reply-To', message.headers.get('Message-ID'));
-msg.setHeader('References', message.headers.get('References') || '');
-msg.addMessage({
- contentType: 'text/plain',
- data: 'Thank you for your message.'
-});
-
-await message.reply(new EmailMessage(
- 'support@example.com',
- message.from,
- msg.asRaw()
-));
-```
-
-**Requirements**:
-- Incoming email needs valid DMARC
-- Reply once per event, recipient = `message.from`
-- Sender domain = receiving domain, with DMARC/SPF/DKIM
-- Max 100 `References` entries
-- Threading: `In-Reply-To` (original Message-ID), `References`, new `Message-ID`
-
-## EmailMessage Constructor
-
-```typescript
-import { EmailMessage } from 'cloudflare:email';
-
-new EmailMessage(from: string, to: string, raw: ReadableStream | string)
-```
-
-Used for sending emails (replies or via SendEmail binding). Domain must be verified.
-
-## SendEmail Interface
-
-```typescript
-interface SendEmail {
- send(message: EmailMessage): Promise;
-}
-
-// Usage
-await env.EMAIL.send(new EmailMessage(from, to, mimeContent));
-```
-
-## SendEmail Binding Types
-
-```jsonc
-{
- "send_email": [
- { "name": "EMAIL" }, // Type 1: Any verified address
- { "name": "LOGS", "destination_address": "logs@example.com" }, // Type 2: Single dest
- { "name": "TEAM", "allowed_destination_addresses": ["a@ex.com", "b@ex.com"] }, // Type 3: Dest allowlist
- { "name": "NOREPLY", "allowed_sender_addresses": ["noreply@ex.com"] } // Type 4: Sender allowlist
- ]
-}
-```
-
-## postal-mime Parsed Output
-
-postal-mime v2.7.3 parses incoming emails into structured data.
-
-```typescript
-interface ParsedEmail {
- headers: Array<{ key: string; value: string }>;
- from: { name: string; address: string } | null;
- to: Array<{ name: string; address: string }> | { name: string; address: string } | null;
- cc: Array<{ name: string; address: string }> | null;
- bcc: Array<{ name: string; address: string }> | null;
- subject: string;
- messageId: string | null;
- inReplyTo: string | null;
- references: string | null;
- date: string | null;
- html: string | null;
- text: string | null;
- attachments: Array<{
- filename: string;
- mimeType: string;
- disposition: string | null;
- related: boolean;
- contentId: string | null;
- content: Uint8Array;
- }>;
-}
-```
-
-### Usage
-
-```typescript
-import PostalMime from 'postal-mime';
-
-const buffer = await new Response(message.raw).arrayBuffer();
-const email = await PostalMime.parse(buffer);
-
-console.log(email.subject);
-console.log(email.from?.address);
-console.log(email.text);
-console.log(email.attachments.length);
-```
-
-## mimetext API Quick Reference
-
-mimetext v3.0.27 composes outgoing emails.
-
-```typescript
-import { createMimeMessage } from 'mimetext';
-
-const msg = createMimeMessage();
-
-// Sender
-msg.setSender({ name: 'John Doe', addr: 'john@example.com' });
-
-// Recipients
-msg.setRecipient('alice@example.com');
-msg.setRecipients(['bob@example.com', 'carol@example.com']);
-msg.setCc('manager@example.com');
-msg.setBcc(['audit@example.com']);
-
-// Headers
-msg.setSubject('Meeting Notes');
-msg.setHeader('In-Reply-To', '');
-msg.setHeader('References', ' ');
-msg.setHeader('Message-ID', `<${crypto.randomUUID()}@example.com>`);
-
-// Content
-msg.addMessage({
- contentType: 'text/plain',
- data: 'Plain text content'
-});
-
-msg.addMessage({
- contentType: 'text/html',
- data: 'HTML content
'
-});
-
-// Attachments
-msg.addAttachment({
- filename: 'report.pdf',
- contentType: 'application/pdf',
- data: pdfBuffer // Uint8Array or base64 string
-});
-
-// Generate raw MIME
-const raw = msg.asRaw(); // Returns string
-```
-
-## TypeScript Types
-
-```typescript
-import {
- ForwardableEmailMessage,
- EmailMessage
-} from 'cloudflare:email';
-
-interface Env {
- EMAIL: SendEmail;
- EMAIL_ARCHIVE: KVNamespace;
- ALLOWED_SENDERS: KVNamespace;
-}
-
-export default {
- async email(
- message: ForwardableEmailMessage,
- env: Env,
- ctx: ExecutionContext
- ): Promise {
- // Fully typed
- }
-};
-```
diff --git a/skills/cloudflare/references/email-workers/configuration.md b/skills/cloudflare/references/email-workers/configuration.md
deleted file mode 100644
index 7928d04..0000000
--- a/skills/cloudflare/references/email-workers/configuration.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# Email Workers Configuration
-
-## wrangler.jsonc
-
-```jsonc
-{
- "name": "email-worker",
- "main": "src/index.ts",
- "compatibility_date": "2025-01-27",
- "send_email": [
- { "name": "EMAIL" }, // Unrestricted
- { "name": "EMAIL_LOGS", "destination_address": "logs@example.com" }, // Single dest
- { "name": "EMAIL_TEAM", "allowed_destination_addresses": ["a@ex.com", "b@ex.com"] },
- { "name": "EMAIL_NOREPLY", "allowed_sender_addresses": ["noreply@ex.com"] }
- ],
- "kv_namespaces": [{ "binding": "ARCHIVE", "id": "xxx" }],
- "r2_buckets": [{ "binding": "ATTACHMENTS", "bucket_name": "email-attachments" }],
- "vars": { "WEBHOOK_URL": "https://hooks.example.com" }
-}
-```
-
-## TypeScript Types
-
-```typescript
-interface Env {
- EMAIL: SendEmail;
- ARCHIVE: KVNamespace;
- ATTACHMENTS: R2Bucket;
- WEBHOOK_URL: string;
-}
-
-export default {
- async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {}
-};
-```
-
-## Dependencies
-
-```bash
-npm install postal-mime mimetext
-npm install -D @cloudflare/workers-types wrangler typescript
-```
-
-Use postal-mime v2.x, mimetext v3.x.
-
-## tsconfig.json
-
-```json
-{
- "compilerOptions": {
- "target": "ES2022", "module": "ES2022", "lib": ["ES2022"],
- "types": ["@cloudflare/workers-types"],
- "moduleResolution": "bundler", "strict": true
- }
-}
-```
-
-## Local Development
-
-```bash
-npx wrangler dev
-
-# Test receiving
-curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \
- --url-query 'from=sender@example.com' --url-query 'to=recipient@example.com' \
- --header 'Content-Type: text/plain' --data-raw 'Subject: Test\n\nHello'
-```
-
-Sent emails write to local `.eml` files.
-
-## Deployment Checklist
-
-- [ ] Enable Email Routing in dashboard
-- [ ] Verify destination addresses
-- [ ] Configure DMARC/SPF/DKIM for sending
-- [ ] Create KV/R2 resources if needed
-- [ ] Update wrangler.jsonc with production IDs
-
-```bash
-npx wrangler deploy
-npx wrangler deployments list
-```
-
-## Dashboard Setup
-
-1. **Email Routing:** Domain → Email → Enable Email Routing
-2. **Verify addresses:** Email → Destination addresses → Add & verify
-3. **Bind Worker:** Email → Email Workers → Create route → Select pattern & Worker
-4. **DMARC:** Add TXT `_dmarc.domain.com`: `v=DMARC1; p=quarantine;`
-
-## Secrets
-
-```bash
-npx wrangler secret put API_KEY
-# Access: env.API_KEY
-```
-
-## Monitoring
-
-```bash
-npx wrangler tail
-npx wrangler tail --status error
-npx wrangler tail --format json
-```
-
-## Troubleshooting
-
-| Error | Fix |
-|-------|-----|
-| "Binding not found" | Check `send_email` name matches code |
-| "Invalid destination" | Verify in Email Routing dashboard |
-| Type errors | Install `@cloudflare/workers-types` |
diff --git a/skills/cloudflare/references/email-workers/gotchas.md b/skills/cloudflare/references/email-workers/gotchas.md
deleted file mode 100644
index 6003038..0000000
--- a/skills/cloudflare/references/email-workers/gotchas.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Email Workers Gotchas
-
-## Critical Issues
-
-### ReadableStream Single-Use
-
-```typescript
-// ❌ WRONG: Stream consumed twice
-const email = await PostalMime.parse(await new Response(message.raw).arrayBuffer());
-const rawText = await new Response(message.raw).text(); // EMPTY!
-
-// ✅ CORRECT: Buffer first
-const buffer = await new Response(message.raw).arrayBuffer();
-const email = await PostalMime.parse(buffer);
-const rawText = new TextDecoder().decode(buffer);
-```
-
-### ctx.waitUntil() Errors Silent
-
-```typescript
-// ❌ Errors dropped silently
-ctx.waitUntil(fetch(webhookUrl, { method: 'POST', body: data }));
-
-// ✅ Catch and log
-ctx.waitUntil(
- fetch(webhookUrl, { method: 'POST', body: data })
- .catch(err => env.ERROR_LOG.put(`error:${Date.now()}`, err.message))
-);
-```
-
-## Security
-
-### Envelope vs Header From (Spoofing)
-
-```typescript
-const envelopeFrom = message.from; // SMTP MAIL FROM (trusted)
-const headerFrom = (await PostalMime.parse(buffer)).from?.address; // (untrusted)
-// Use envelope for security decisions
-```
-
-### Input Validation
-
-```typescript
-if (message.rawSize > 5_000_000) { message.setReject('Too large'); return; }
-if ((message.headers.get('Subject') || '').length > 1000) {
- message.setReject('Invalid subject'); return;
-}
-```
-
-### DMARC for Replies
-
-Replies fail silently without DMARC. Verify: `dig TXT _dmarc.example.com`
-
-## Parsing
-
-### Address Parsing
-
-```typescript
-const email = await PostalMime.parse(buffer);
-const fromAddress = email.from?.address || 'unknown';
-const toAddresses = Array.isArray(email.to) ? email.to.map(t => t.address) : [email.to?.address];
-```
-
-### Character Encoding
-
-Let postal-mime handle decoding - `email.subject`, `email.text`, `email.html` are UTF-8.
-
-## API Behavior
-
-### setReject() vs throw
-
-```typescript
-// setReject() for SMTP rejection
-if (blockList.includes(message.from)) { message.setReject('Blocked'); return; }
-
-// throw for worker errors
-if (!env.KV) throw new Error('KV not configured');
-```
-
-### forward() Only X-* Headers
-
-```typescript
-headers.set('X-Processed-By', 'worker'); // ✅ Works
-headers.set('Subject', 'Modified'); // ❌ Dropped
-```
-
-### Reply Requires Verified Domain
-
-```typescript
-// Use same domain as receiving address
-const receivingDomain = message.to.split('@')[1];
-await message.reply(new EmailMessage(`noreply@${receivingDomain}`, message.from, rawMime));
-```
-
-## Performance
-
-### CPU Limit
-
-```typescript
-// Skip parsing large emails
-if (message.rawSize > 5_000_000) {
- await message.forward('inbox@example.com');
- return;
-}
-```
-
-Monitor: `npx wrangler tail`
-
-## Limits
-
-| Limit | Value |
-|-------|-------|
-| Max message size | 25 MiB |
-| Max rules/zone | 200 |
-| CPU time (free/paid) | 10ms / 30s default, 5min max |
-| Reply References | 100 |
-
-## Common Errors
-
-| Error | Fix |
-|-------|-----|
-| "Address not verified" | Add in Email Routing dashboard |
-| "Exceeded CPU time" | Use `ctx.waitUntil()` or upgrade |
-| "Stream is locked" | Buffer `message.raw` first |
-| Silent reply failure | Check DMARC records |
diff --git a/skills/cloudflare/references/email-workers/patterns.md b/skills/cloudflare/references/email-workers/patterns.md
deleted file mode 100644
index f1e65f5..0000000
--- a/skills/cloudflare/references/email-workers/patterns.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Email Workers Patterns
-
-## Parse Email
-
-```typescript
-import PostalMime from 'postal-mime';
-
-export default {
- async email(message, env, ctx) {
- const buffer = await new Response(message.raw).arrayBuffer();
- const email = await PostalMime.parse(buffer);
- console.log(email.from, email.subject, email.text, email.attachments.length);
- await message.forward('inbox@example.com');
- }
-};
-```
-
-## Filtering
-
-```typescript
-// Allowlist from KV
-const allowList = await env.ALLOWED_SENDERS.get('list', 'json') || [];
-if (!allowList.includes(message.from)) {
- message.setReject('Not allowed');
- return;
-}
-
-// Size check (avoid parsing large emails)
-if (message.rawSize > 5_000_000) {
- await message.forward('inbox@example.com'); // Forward without parsing
- return;
-}
-```
-
-## Auto-Reply with Threading
-
-```typescript
-import { EmailMessage } from 'cloudflare:email';
-import { createMimeMessage } from 'mimetext';
-
-const msg = createMimeMessage();
-msg.setSender({ addr: 'support@example.com' });
-msg.setRecipient(message.from);
-msg.setSubject(`Re: ${message.headers.get('Subject')}`);
-msg.setHeader('In-Reply-To', message.headers.get('Message-ID') || '');
-msg.addMessage({ contentType: 'text/plain', data: 'Thank you. We will respond.' });
-
-await message.reply(new EmailMessage('support@example.com', message.from, msg.asRaw()));
-```
-
-## Rate-Limited Auto-Reply
-
-```typescript
-const rateKey = `rate:${message.from}`;
-if (!await env.RATE_LIMIT.get(rateKey)) {
- // Send reply...
- ctx.waitUntil(env.RATE_LIMIT.put(rateKey, '1', { expirationTtl: 3600 }));
-}
-```
-
-## Subject-Based Routing
-
-```typescript
-const subject = (message.headers.get('Subject') || '').toLowerCase();
-if (subject.includes('billing')) await message.forward('billing@example.com');
-else if (subject.includes('support')) await message.forward('support@example.com');
-else await message.forward('general@example.com');
-```
-
-## Multi-Tenant Routing
-
-```typescript
-// support+tenant123@example.com → tenant123
-const tenantId = message.to.split('@')[0].match(/\+(.+)$/)?.[1] || 'default';
-const config = await env.TENANT_CONFIG.get(tenantId, 'json');
-config?.forwardTo ? await message.forward(config.forwardTo) : message.setReject('Unknown');
-```
-
-## Archive & Extract Attachments
-
-```typescript
-// Archive to KV
-ctx.waitUntil(env.ARCHIVE.put(`email:${Date.now()}`, JSON.stringify({
- from: message.from, subject: email.subject
-})));
-
-// Attachments to R2
-for (const att of email.attachments) {
- ctx.waitUntil(env.R2.put(`${Date.now()}-${att.filename}`, att.content));
-}
-```
-
-## Webhook Integration
-
-```typescript
-ctx.waitUntil(
- fetch(env.WEBHOOK_URL, {
- method: 'POST',
- body: JSON.stringify({ from: message.from, subject: message.headers.get('Subject') })
- }).catch(err => console.error(err))
-);
-```
diff --git a/skills/cloudflare/references/flagship/README.md b/skills/cloudflare/references/flagship/README.md
deleted file mode 100644
index c4802cc..0000000
--- a/skills/cloudflare/references/flagship/README.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Cloudflare Flagship
-
-Feature flag service for controlling feature visibility without redeploying code. Define flags with targeting rules and percentage-based rollouts, then evaluate them in Workers via a native binding or from any JavaScript runtime via the OpenFeature SDK.
-
-## When to Use
-
-| Need | Use Flagship? | Alternative |
-|------|--------------|-------------|
-| Feature toggles (on/off) | Yes | — |
-| Gradual rollouts (percentage-based) | Yes | — |
-| A/B testing with attribute targeting | Yes | — |
-| Multi-variant configuration delivery | Yes | — |
-| Environment-specific config (dev/staging/prod) | Consider | Wrangler environments, secrets |
-| Static config that never changes | No | `wrangler.jsonc` vars |
-| Per-request rate limiting | No | Rate Limiting rules |
-
-## Key Concepts
-
-- **Apps** — Top-level organizational unit. Maps to a project or service. Each account can have multiple apps.
-- **Flags** — Named feature toggles with a key, variations, targeting rules, and enabled/disabled state.
-- **Variations** — Possible values a flag returns. Types: boolean, string, number, JSON object. All variations on a flag must share the same type.
-- **Targeting rules** — Sequential, priority-ordered conditions that determine which variation to serve. First match wins; no match returns the default.
-- **Evaluation context** — Key-value attributes (`userId`, `country`, `plan`, etc.) passed at evaluation time for rule matching and rollout bucketing.
-- **Percentage rollouts** — Gradually release to a fraction of users. Consistent hashing on a configurable attribute ensures sticky bucketing.
-
-## Two Evaluation Paths
-
-| Path | Runtime | Package | Latency | Auth |
-|------|---------|---------|---------|------|
-| **Binding** (`env.FLAGS`) | Workers only | `@cloudflare/workers-types` | Lowest (no HTTP) | Automatic via binding |
-| **OpenFeature SDK** | Workers, Node.js, browser | `@cloudflare/flagship` + `@openfeature/server-sdk` or `@openfeature/web-sdk` | HTTP per eval (server) or prefetch (client) | API token or binding passthrough |
-
-**Recommendation:** Use the binding inside Workers. Use the SDK when running outside Workers or when you need OpenFeature vendor-neutrality.
-
-## Reading Order
-
-| Task | Read |
-|------|------|
-| Set up Flagship in a Worker | `configuration.md` → `api.md` |
-| Evaluate flags in code | `configuration.md` → `patterns.md` |
-| Manage flags via REST API | `api.md` → `patterns.md` |
-| Design targeting rules & rollouts | `patterns.md` → `gotchas.md` |
-| Debug flag evaluation issues | `gotchas.md` → `api.md` |
-
-REST API note: management endpoints use Cloudflare v4 envelopes (`result`, `result_info`, `errors`) and snake_case fields. The `/evaluate` endpoint is the exception: it is not enveloped and returns OpenFeature-style camelCase.
-
-## In This Reference
-
-- **[api.md](./api.md)** — REST API endpoints, binding methods, OpenFeature SDK, schemas
-- **[configuration.md](./configuration.md)** — Wrangler binding setup, SDK installation, TypeScript types
-- **[patterns.md](./patterns.md)** — Flag CRUD via API, targeting rules, rollouts, OpenFeature usage
-- **[gotchas.md](./gotchas.md)** — Common errors, limits, anti-patterns, troubleshooting
-
-## See Also
-
-- **[Flagship API reference](https://developers.cloudflare.com/api/resources/flagship/)** — Source of truth for REST API paths, envelopes, and response fields
-- **[../workers/](../workers/)** — Workers runtime (Flagship runs inside Workers)
-- **[../kv/](../kv/)** — KV storage (Flagship uses KV infrastructure for flag delivery)
-- **[../wrangler/](../wrangler/)** — Wrangler CLI for deployment and config
diff --git a/skills/cloudflare/references/flagship/api.md b/skills/cloudflare/references/flagship/api.md
deleted file mode 100644
index a9d05e1..0000000
--- a/skills/cloudflare/references/flagship/api.md
+++ /dev/null
@@ -1,390 +0,0 @@
-# Flagship API Reference
-
-## Binding API (Workers)
-
-The binding is available as `env.FLAGS` (type `Flagship` from `@cloudflare/workers-types`).
-
-### Evaluation Methods
-
-All methods are async, never throw, and return the `defaultValue` on errors.
-
-| Method | Signature | Returns |
-|--------|-----------|---------|
-| `get` | `get(flagKey, defaultValue?, context?)` | `Promise` |
-| `getBooleanValue` | `getBooleanValue(flagKey, defaultValue, context?)` | `Promise` |
-| `getStringValue` | `getStringValue(flagKey, defaultValue, context?)` | `Promise` |
-| `getNumberValue` | `getNumberValue(flagKey, defaultValue, context?)` | `Promise` |
-| `getObjectValue` | `getObjectValue(flagKey, defaultValue, context?)` | `Promise` |
-| `getBooleanDetails` | `getBooleanDetails(flagKey, defaultValue, context?)` | `Promise>` |
-| `getStringDetails` | `getStringDetails(flagKey, defaultValue, context?)` | `Promise>` |
-| `getNumberDetails` | `getNumberDetails(flagKey, defaultValue, context?)` | `Promise>` |
-| `getObjectDetails` | `getObjectDetails(flagKey, defaultValue, context?)` | `Promise>` |
-
-### Parameters (shared across all methods)
-
-| Parameter | Type | Required | Description |
-|-----------|------|----------|-------------|
-| `flagKey` | `string` | Yes | Flag key to evaluate |
-| `defaultValue` | varies | Yes (except `get`) | Fallback if evaluation fails or flag not found |
-| `context` | `FlagshipEvaluationContext` | No | Attributes for targeting rules (`{ userId: "user-42", country: "US" }`) |
-
-### Types
-
-```typescript
-type FlagshipEvaluationContext = Record;
-
-interface FlagshipEvaluationDetails {
- flagKey: string;
- value: T;
- variant?: string; // name of the matched variation
- reason?: string; // "TARGETING_MATCH" | "DEFAULT" | "DISABLED" | "SPLIT"
- errorCode?: string; // "TYPE_MISMATCH" | "GENERAL"
- errorMessage?: string;
-}
-```
-
-### Example
-
-```typescript
-export default {
- async fetch(request: Request, env: Env): Promise {
- const enabled = await env.FLAGS.getBooleanValue("new-feature", false, {
- userId: "user-42",
- });
- return new Response(enabled ? "Feature on" : "Feature off");
- },
-};
-```
-
----
-
-## OpenFeature SDK
-
-Package: `@cloudflare/flagship`
-
-### Server Provider (`FlagshipServerProvider`)
-
-For Workers, Node.js, and server-side JavaScript.
-
-**With binding (recommended inside Workers):**
-
-```typescript
-import { OpenFeature } from "@openfeature/server-sdk";
-import { FlagshipServerProvider } from "@cloudflare/flagship";
-
-await OpenFeature.setProviderAndWait(
- new FlagshipServerProvider({ binding: env.FLAGS }),
-);
-const client = OpenFeature.getClient();
-const enabled = await client.getBooleanValue("new-checkout", false, {
- targetingKey: "user-42",
-});
-```
-
-**With app ID (Node.js / non-Worker runtimes):**
-
-```typescript
-import { OpenFeature } from "@openfeature/server-sdk";
-import { FlagshipServerProvider } from "@cloudflare/flagship";
-
-await OpenFeature.setProviderAndWait(
- new FlagshipServerProvider({
- appId: "",
- accountId: "",
- authToken: "",
- }),
-);
-const client = OpenFeature.getClient();
-const enabled = await client.getBooleanValue("new-checkout", false, {
- targetingKey: "user-42",
-});
-```
-
-### Client Provider (`FlagshipClientProvider`)
-
-For browser applications. Pre-fetches flags on init, evaluates synchronously.
-
-```typescript
-import { OpenFeature } from "@openfeature/web-sdk";
-import { FlagshipClientProvider } from "@cloudflare/flagship";
-
-await OpenFeature.setProviderAndWait(
- new FlagshipClientProvider({
- appId: "",
- accountId: "",
- authToken: "",
- prefetchFlags: ["promo-banner", "dark-mode"],
- }),
-);
-await OpenFeature.setContext({ targetingKey: "user-42", plan: "enterprise" });
-const client = OpenFeature.getClient();
-
-// Synchronous — no await needed
-const showBanner = client.getBooleanValue("promo-banner", false);
-```
-
-**Important:** Only flags listed in `prefetchFlags` are available. Unlisted flags return `FLAG_NOT_FOUND`.
-
-### SDK Hooks
-
-```typescript
-import { LoggingHook, TelemetryHook } from "@cloudflare/flagship";
-OpenFeature.addHooks(new LoggingHook(), new TelemetryHook());
-```
-
----
-
-## REST API (Flag Management)
-
-Source of truth: [Cloudflare Flagship API reference](https://developers.cloudflare.com/api/resources/flagship/). Use it to verify REST paths, envelopes, response fields, and permission wording before relying on examples here.
-
-### FIRST: Check Prerequisites
-
-Before making any REST API calls (create, read, update, delete, toggle flags), verify these environment variables are set:
-
-| Variable | Purpose | How to get |
-|----------|---------|------------|
-| `CLOUDFLARE_ACCOUNT_ID` | Account identifier | Dashboard URL or `wrangler whoami` |
-| `CLOUDFLARE_API_TOKEN` | Bearer token for API auth | [Create API token](https://dash.cloudflare.com/profile/api-tokens) with Flagship permissions |
-| `FLAGSHIP_APP_ID` | Target app UUID | Dashboard under **Compute > Flagship**, or `GET /apps` endpoint |
-
-Check with:
-
-```bash
-echo "CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID:-(not set)}"
-echo "CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:-(not set)}"
-echo "FLAGSHIP_APP_ID=${FLAGSHIP_APP_ID:-(not set)}"
-```
-
-**If any are missing, ask the user to provide them before proceeding.**
-
-### Base URL and Auth
-
-Base URL: `https://api.cloudflare.com/client/v4/accounts/{account_id}/flagship`
-
-Authentication: `Authorization: Bearer `
-
-Management endpoints use the Cloudflare v4 envelope. On success, the payload is under `result`; errors are an array under `errors`.
-
-```jsonc
-// Success
-{ "success": true, "result": , "errors": [], "messages": [] }
-
-// Paginated success
-{
- "success": true,
- "result": [],
- "result_info": { "count": 50, "cursor": "next-cursor-or-null" },
- "errors": [],
- "messages": []
-}
-
-// Error
-{ "success": false, "result": null, "errors": [{ "message": "message" }], "messages": [] }
-```
-
-### App Endpoints
-
-| Method | Path | Description |
-|--------|------|-------------|
-| `GET` | `/apps` | List all apps |
-| `GET` | `/apps/{app_id}` | Get app |
-| `POST` | `/apps` | Create app (`{ "name": "my-app" }`) |
-| `PUT` | `/apps/{app_id}` | Update app (`{ "name": "new-name" }`) |
-| `DELETE` | `/apps/{app_id}` | Delete app |
-
-App name constraints: alphanumeric + hyphens + underscores, 1-64 chars.
-
-### Flag Endpoints
-
-| Method | Path | Description |
-|--------|------|-------------|
-| `GET` | `/apps/{app_id}/flags?limit=50&cursor=` | List flags (paginated) |
-| `GET` | `/apps/{app_id}/flags/{flag_key}` | Get flag |
-| `POST` | `/apps/{app_id}/flags` | Create flag |
-| `PUT` | `/apps/{app_id}/flags/{flag_key}` | Update flag (full replace) |
-| `DELETE` | `/apps/{app_id}/flags/{flag_key}` | Delete flag |
-| `GET` | `/apps/{app_id}/flags/{flag_key}/changelog?limit=20&cursor=` | Flag changelog |
-
-### Evaluate Endpoint
-
-```
-GET /apps/{app_id}/evaluate?flagKey=&
-```
-
-Requires an API token with the `com.cloudflare.account.flagship.evaluate` permission. Context attributes passed as query params. This endpoint is not wrapped in the management envelope; the SDK contract returns OpenFeature-style camelCase:
-
-```json
-{
- "flagKey": "my-flag",
- "value": true,
- "variant": "on",
- "reason": "SPLIT"
-}
-```
-
-Reasons: `TARGETING_MATCH`, `SPLIT`, `DEFAULT`, `DISABLED`.
-
-### Management Response Payloads
-
-Management endpoints are wrapped in the Cloudflare v4 envelope shown above. Common `.result` payloads:
-
-**App result**
-
-```json
-{
- "id": "app-uuid",
- "name": "my-app",
- "created_at": "2026-06-09T12:00:00.000Z",
- "updated_at": "2026-06-09T12:00:00.000Z",
- "updated_by": "user@example.com"
-}
-```
-
-**Flag result**
-
-```json
-{
- "key": "my-flag",
- "type": "boolean",
- "default_variation": "off",
- "variations": { "on": true, "off": false },
- "rules": [],
- "description": "Enables the new feature",
- "enabled": true,
- "updated_at": "2026-06-09T12:00:00.000Z",
- "updated_by": "user@example.com"
-}
-```
-
-**Changelog entry**
-
-```json
-{
- "flag_key": "my-flag",
- "event": "update",
- "after": { "key": "my-flag", "default_variation": "off", "variations": { "on": true, "off": false }, "rules": [], "enabled": true },
- "diff": { "enabled": { "from": false, "to": true } }
-}
-```
-
-Changelog entries include the full flag state after the change. `update` entries also include `diff`.
-
----
-
-## FlagDefinition Schema
-
-```json
-{
- "key": "my-flag",
- "type": "boolean",
- "default_variation": "off",
- "variations": {
- "on": true,
- "off": false
- },
- "rules": [
- {
- "priority": 1,
- "conditions": [
- {
- "attribute": "email",
- "operator": "ends_with",
- "value": "@cloudflare.com"
- }
- ],
- "serve_variation": "on",
- "rollout": { "percentage": 100 }
- }
- ],
- "description": "Enables the new feature",
- "enabled": true
-}
-```
-
-### Field Constraints
-
-| Field | Type | Constraints |
-|-------|------|-------------|
-| `key` | string | 1-64 chars, `/^[a-zA-Z0-9_-]+$/` |
-| `type` | enum | Optional. `boolean`, `string`, `number`, `json` (auto-inferred from variations) |
-| `default_variation` | string | Must be a key in `variations` |
-| `variations` | `Record` | At least one. All values same type. Keys: alphanumeric/hyphens/underscores, max 64 chars. Values max 10KB. |
-| `rules` | `Rule[]` | Can be empty. No duplicate priorities. |
-| `description` | string? | Max 512 chars, nullable |
-| `enabled` | boolean | Required. `false` = always returns default variation. |
-
-### Rule Schema
-
-```json
-{
- "priority": 1,
- "conditions": [ /* Condition[] */ ],
- "serve_variation": "on",
- "rollout": { "percentage": 50, "attribute": "targetingKey" }
-}
-```
-
-- `priority`: integer >= 1, unique across rules in the flag (lower = evaluated first)
-- `conditions`: array of base or logical conditions
-- `serve_variation`: must be a key in `variations`
-- `rollout`: optional. `percentage` 0-100. `attribute` defaults to `targetingKey`.
-
-### Condition Schema
-
-**Base condition:**
-
-```json
-{ "attribute": "email", "operator": "ends_with", "value": "@cloudflare.com" }
-```
-
-**Logical condition (AND/OR):**
-
-```json
-{
- "logical_operator": "AND",
- "clauses": [
- { "attribute": "country", "operator": "equals", "value": "US" },
- { "attribute": "plan", "operator": "in", "value": ["enterprise", "business"] }
- ]
-}
-```
-
-Nesting supported up to 6 levels deep.
-
-### Operators
-
-| Operator | Description | Value Type |
-|----------|-------------|------------|
-| `equals` | Exact match (case-sensitive) | String |
-| `not_equals` | Not exact match | String |
-| `greater_than` | Numeric / datetime > | Number, ISO 8601 |
-| `less_than` | Numeric / datetime < | Number, ISO 8601 |
-| `greater_than_or_equals` | >= | Number, ISO 8601 |
-| `less_than_or_equals` | <= | Number, ISO 8601 |
-| `contains` | Substring match (case-sensitive) | String |
-| `starts_with` | Prefix match | String |
-| `ends_with` | Suffix match | String |
-| `in` | Value in array | Array |
-| `not_in` | Value not in array | Array |
-
----
-
-## Rate Limits
-
-| Operation | Limit |
-|-----------|-------|
-| Mutations (POST/PUT/DELETE) | 60 per 60s per account:app |
-| Reads (GET) | 600 per 60s per account:app |
-
-## Error Codes
-
-| HTTP Status | Meaning |
-|-------------|---------|
-| 200 | Success (read/update/delete) |
-| 201 | Created (create) |
-| 400 | Validation error (check `errors[].message`) |
-| 401 | Invalid or missing token |
-| 404 | Flag or app not found |
-| 409 | Flag key already exists (create) |
-| 429 | Rate limited |
diff --git a/skills/cloudflare/references/flagship/configuration.md b/skills/cloudflare/references/flagship/configuration.md
deleted file mode 100644
index 8da90d5..0000000
--- a/skills/cloudflare/references/flagship/configuration.md
+++ /dev/null
@@ -1,202 +0,0 @@
-# Flagship Configuration
-
-## Wrangler Binding Setup
-
-Add a Flagship binding to your Wrangler config to access flags via `env.FLAGS`.
-
-### Single App
-
-```jsonc
-// wrangler.jsonc
-{
- "flagship": {
- "binding": "FLAGS",
- "app_id": ""
- }
-}
-```
-
-```toml
-# wrangler.toml
-[flagship]
-binding = "FLAGS"
-app_id = ""
-```
-
-### Multiple Apps
-
-```jsonc
-// wrangler.jsonc
-{
- "flagship": [
- {
- "binding": "FLAGS",
- "app_id": ""
- },
- {
- "binding": "EXPERIMENT_FLAGS",
- "app_id": ""
- }
- ]
-}
-```
-
-```toml
-# wrangler.toml
-[[flagship]]
-binding = "FLAGS"
-app_id = ""
-
-[[flagship]]
-binding = "EXPERIMENT_FLAGS"
-app_id = ""
-```
-
-### Generate Types
-
-After adding the binding, generate TypeScript types:
-
-```bash
-npx wrangler types
-```
-
-This creates the `Env` interface with each binding typed as `Flagship`:
-
-```typescript
-interface Env {
- FLAGS: Flagship;
- EXPERIMENT_FLAGS: Flagship; // if multiple
-}
-```
-
-The `Flagship` type comes from `@cloudflare/workers-types`.
-
----
-
-## OpenFeature SDK Installation
-
-### Server-Side (Workers, Node.js)
-
-```bash
-npm i @cloudflare/flagship @openfeature/server-sdk
-```
-
-### Browser
-
-```bash
-npm i @cloudflare/flagship @openfeature/web-sdk
-```
-
----
-
-## SDK Provider Setup
-
-### Server Provider — With Binding (Workers)
-
-Recommended approach inside Workers. No HTTP overhead, auth handled automatically.
-
-```typescript
-import { OpenFeature } from "@openfeature/server-sdk";
-import { FlagshipServerProvider } from "@cloudflare/flagship";
-
-export default {
- async fetch(request: Request, env: Env): Promise {
- await OpenFeature.setProviderAndWait(
- new FlagshipServerProvider({ binding: env.FLAGS }),
- );
- const client = OpenFeature.getClient();
- // ... evaluate flags
- },
-};
-```
-
-### Server Provider — With App ID (Node.js)
-
-For non-Worker runtimes. Requires an API token with Flagship read permissions.
-
-```typescript
-import { OpenFeature } from "@openfeature/server-sdk";
-import { FlagshipServerProvider } from "@cloudflare/flagship";
-
-await OpenFeature.setProviderAndWait(
- new FlagshipServerProvider({
- appId: "",
- accountId: "",
- authToken: "",
- }),
-);
-const client = OpenFeature.getClient();
-```
-
-### Client Provider (Browser)
-
-Pre-fetches flags on init, then evaluates synchronously. Only `prefetchFlags` are available.
-
-```typescript
-import { OpenFeature } from "@openfeature/web-sdk";
-import { FlagshipClientProvider } from "@cloudflare/flagship";
-
-await OpenFeature.setProviderAndWait(
- new FlagshipClientProvider({
- appId: "",
- accountId: "",
- authToken: "",
- prefetchFlags: ["promo-banner", "dark-mode", "max-uploads"],
- }),
-);
-await OpenFeature.setContext({ targetingKey: "user-42", plan: "enterprise" });
-const client = OpenFeature.getClient();
-```
-
-### Provider Options Reference
-
-**FlagshipServerProvider:**
-
-| Option | Type | Required | Description |
-|--------|------|----------|-------------|
-| `binding` | `Flagship` | No | Binding from `env.FLAGS`. Use inside Workers. |
-| `appId` | string | No | App ID from dashboard. Required without binding. |
-| `accountId` | string | No | Cloudflare account ID. Required without binding. |
-| `authToken` | string | No | API token with Flagship read permissions. Required without binding. |
-
-Provide either `binding` or all three of `appId` + `accountId` + `authToken`.
-
-**FlagshipClientProvider:**
-
-| Option | Type | Required | Description |
-|--------|------|----------|-------------|
-| `appId` | string | Yes | App ID from dashboard |
-| `accountId` | string | Yes | Cloudflare account ID |
-| `authToken` | string | Yes | API token with Flagship read permissions |
-| `prefetchFlags` | string[] | Yes | Flag keys to prefetch. Unlisted flags return `FLAG_NOT_FOUND`. |
-
----
-
-## REST API Authentication
-
-For managing flags via the REST API (create, update, delete), set these environment variables:
-
-| Variable | Description |
-|----------|-------------|
-| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |
-| `CLOUDFLARE_API_TOKEN` | API token with Flagship permissions |
-| `FLAGSHIP_APP_ID` | Target app UUID (from dashboard under **Compute > Flagship**, or `GET /apps`) |
-
-Base URL: `https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship`
-
-```bash
-curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps" | jq .
-```
-
-App IDs are shown in the Cloudflare dashboard under **Compute > Flagship**.
-
----
-
-## Local Development
-
-Flagship bindings work in local dev with `wrangler dev`. Flag evaluation uses the live Flagship configuration — there is no local flag store. Ensure the `app_id` in your Wrangler config points to a valid app.
-
-```bash
-npx wrangler dev
-```
diff --git a/skills/cloudflare/references/flagship/gotchas.md b/skills/cloudflare/references/flagship/gotchas.md
deleted file mode 100644
index 52429a6..0000000
--- a/skills/cloudflare/references/flagship/gotchas.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# Flagship Gotchas & Troubleshooting
-
-## Common Errors
-
-### Flag Always Returns Default Value
-
-**Cause:** Flag is disabled (`enabled: false`), or no targeting rules match, or evaluation context is missing expected attributes.
-
-**Solution:** Check these in order:
-
-1. Is the flag enabled? (`"enabled": true`)
-2. Do your targeting rules match the context you're passing?
-3. Are you passing the right attributes in the evaluation context?
-
-```typescript
-// ❌ BAD — no context, rules can't match
-const val = await env.FLAGS.getBooleanValue("my-flag", false);
-
-// ✅ GOOD — pass context attributes that rules reference
-const val = await env.FLAGS.getBooleanValue("my-flag", false, {
- userId: "user-42",
- plan: "enterprise",
-});
-```
-
-### TYPE_MISMATCH Error in Details
-
-**Cause:** Calling a typed method on a flag with a different type (e.g., `getBooleanValue` on a string flag).
-
-**Solution:** Use the method matching the flag's variation type.
-
-```typescript
-// ❌ BAD — flag "checkout-flow" has string variations
-const val = await env.FLAGS.getBooleanValue("checkout-flow", false);
-
-// ✅ GOOD
-const val = await env.FLAGS.getStringValue("checkout-flow", "original");
-```
-
-### 409 Conflict on Flag Creation
-
-**Cause:** A flag with that key already exists in the app.
-
-**Solution:** Use a different key, or GET + PUT to update the existing flag.
-
-### Inconsistent Rollout Results
-
-**Cause:** `targetingKey` (or the configured bucketing attribute) is missing from the evaluation context, causing random bucketing on each request.
-
-**Solution:** Always pass a stable identifier:
-
-```typescript
-// ❌ BAD — no targetingKey, rollout is random per request
-const val = await env.FLAGS.getBooleanValue("gradual-rollout", false);
-
-// ✅ GOOD — stable userId for consistent bucketing
-const val = await env.FLAGS.getBooleanValue("gradual-rollout", false, {
- userId: sessionUserId,
-});
-```
-
-### Update Overwrites Entire Flag
-
-**Cause:** PUT replaces the full `FlagDefinition`. Sending only changed fields deletes the rest.
-
-**Solution:** Always read-modify-write:
-
-```bash
-# ❌ BAD — overwrites the entire flag, losing rules/variations
-curl -X PUT -d '{"enabled": true}' ...
-
-# ✅ GOOD — GET first, modify, PUT back
-FLAG=$(curl -s -H "Authorization: Bearer $TOKEN" "$URL/flags/my-flag" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.enabled = true')
-echo "$UPDATED" | curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @- "$URL/flags/my-flag"
-```
-
-### Reading REST Envelope Fields
-
-**Cause:** Management endpoints use Cloudflare v4 envelopes, not raw payloads.
-
-**Solution:** Read `.result` for successful payloads, `.result_info.cursor` for pagination, and `.errors[].message` for errors.
-
-```bash
-jq '.result'
-jq '.result_info.cursor'
-jq '.errors[].message'
-```
-
-### Mixing CamelCase and Snake Case in REST Responses
-
-**Cause:** Management API responses are public API JSON and use snake_case. Evaluation responses use OpenFeature-style camelCase.
-
-**Solution:** For management endpoints use `default_variation`, `serve_variation`, `updated_at`, `updated_by`, and changelog `flag_key`. For `/evaluate`, use `flagKey`, `variant`, and `reason`.
-
-### FLAG_NOT_FOUND in Client Provider
-
-**Cause:** Flag key not included in `prefetchFlags` array.
-
-**Solution:** Add the flag key to `prefetchFlags` when initializing `FlagshipClientProvider`.
-
-### Client Provider Token Exposure
-
-**Cause:** The `authToken` passed to `FlagshipClientProvider` is visible in the browser. It can evaluate flags across all apps in the account.
-
-**Solution:** Use a token with minimal permissions (Flagship Evaluate only). Never use a token with write/management permissions in the browser.
-
----
-
-## Limits
-
-| Limit | Value | Notes |
-|-------|-------|-------|
-| Flag key length | 1-64 chars | Alphanumeric, hyphens, underscores only |
-| Flag key pattern | `/^[a-zA-Z0-9_-]+$/` | — |
-| Variation value size | 10KB max | Per variation, serialized |
-| Variation name length | 64 chars max | Alphanumeric, hyphens, underscores |
-| Description length | 512 chars max | Nullable |
-| App name length | 1-64 chars | Alphanumeric, hyphens, underscores |
-| Logical nesting depth | 6 levels | AND/OR conditions |
-| Mutation rate limit | 60 / 60s | Per account:app |
-| Read rate limit | 600 / 60s | Per account:app |
-| Rollout percentage | 0-100 | Integer |
-| Rule priorities | Unique integers >= 1 | Lower = evaluated first |
-
----
-
-## Anti-Patterns
-
-### Evaluating Flags in a Tight Loop
-
-Flag evaluation via the binding is fast but not free. Avoid evaluating the same flag repeatedly in a loop — evaluate once and reuse the result.
-
-```typescript
-// ❌ BAD
-for (const item of items) {
- const enabled = await env.FLAGS.getBooleanValue("my-flag", false, ctx);
- // ...
-}
-
-// ✅ GOOD
-const enabled = await env.FLAGS.getBooleanValue("my-flag", false, ctx);
-for (const item of items) {
- // use `enabled`
-}
-```
-
-### Using the SDK Inside Workers When Binding Is Available
-
-The binding avoids HTTP overhead entirely. Only use the SDK inside Workers when you specifically need OpenFeature vendor-neutrality.
-
-```typescript
-// ❌ Unnecessary HTTP overhead inside a Worker
-const provider = new FlagshipServerProvider({
- appId: "...", accountId: "...", authToken: "...",
-});
-
-// ✅ Use the binding directly, or pass it to the SDK
-const provider = new FlagshipServerProvider({ binding: env.FLAGS });
-```
-
-### Partial PUT Updates
-
-The flag update API (PUT) requires the complete `FlagDefinition`. Sending only changed fields silently drops everything else. Always GET first, then modify and PUT back the full object.
-
-### Stale Flag Cleanup
-
-Flags that are disabled and no longer referenced in code should be deleted. Stale flags clutter the dashboard and make it harder to understand which flags are active. Follow the safe deletion workflow in `patterns.md`.
-
----
-
-## Propagation Behavior
-
-Flag changes propagate globally within seconds. During the brief propagation window, some regions may serve the previous value. After propagation completes, all evaluations return the updated value.
-
-- No Worker redeployment needed for flag changes.
-- If the dashboard is temporarily unavailable, evaluation continues using the last propagated configuration.
-- Flag changes made via the REST API and dashboard are equivalent — both trigger propagation.
diff --git a/skills/cloudflare/references/flagship/patterns.md b/skills/cloudflare/references/flagship/patterns.md
deleted file mode 100644
index 06fdbbd..0000000
--- a/skills/cloudflare/references/flagship/patterns.md
+++ /dev/null
@@ -1,469 +0,0 @@
-# Flagship Patterns & Best Practices
-
-## Evaluating Flags in Workers (Binding)
-
-### Simple Boolean Toggle
-
-```typescript
-export default {
- async fetch(request: Request, env: Env): Promise {
- const showNewUI = await env.FLAGS.getBooleanValue("new-ui", false, {
- userId: "user-42",
- });
-
- if (showNewUI) {
- return new Response("New UI");
- }
- return new Response("Classic UI");
- },
-};
-```
-
-### Multi-Variant String Flag
-
-```typescript
-const checkoutFlow = await env.FLAGS.getStringValue(
- "checkout-flow",
- "original",
- { userId, country: "US" },
-);
-
-switch (checkoutFlow) {
- case "streamlined":
- return handleStreamlined(request);
- case "one-click":
- return handleOneClick(request);
- default:
- return handleOriginal(request);
-}
-```
-
-### JSON Config Flag
-
-```typescript
-interface RateLimitConfig {
- rpm: number;
- burst: number;
-}
-
-const limits = await env.FLAGS.getObjectValue(
- "rate-limits",
- { rpm: 100, burst: 20 },
- { plan: userPlan },
-);
-```
-
-### Using Details for Observability
-
-```typescript
-const details = await env.FLAGS.getBooleanDetails("new-checkout", false, {
- userId: "user-42",
-});
-
-console.log(details.value); // true
-console.log(details.variant); // "on"
-console.log(details.reason); // "TARGETING_MATCH"
-console.log(details.errorCode); // undefined (no error)
-```
-
----
-
-## Evaluating Flags with OpenFeature (Workers)
-
-### Binding Passthrough (Recommended)
-
-```typescript
-import { OpenFeature } from "@openfeature/server-sdk";
-import { FlagshipServerProvider } from "@cloudflare/flagship";
-
-export default {
- async fetch(request: Request, env: Env): Promise {
- await OpenFeature.setProviderAndWait(
- new FlagshipServerProvider({ binding: env.FLAGS }),
- );
- const client = OpenFeature.getClient();
-
- const enabled = await client.getBooleanValue("new-checkout", false, {
- targetingKey: "user-42",
- plan: "enterprise",
- country: "US",
- });
-
- return new Response(enabled ? "New checkout" : "Standard checkout");
- },
-};
-```
-
-### Migration from Another Provider
-
-Only the provider initialization changes — evaluation call sites stay the same:
-
-```typescript
-// ❌ Before (LaunchDarkly)
-await OpenFeature.setProviderAndWait(
- new LaunchDarklyProvider({ sdkKey: "..." }),
-);
-
-// ✅ After (Flagship)
-await OpenFeature.setProviderAndWait(
- new FlagshipServerProvider({ binding: env.FLAGS }),
-);
-
-// Evaluation code is unchanged
-const enabled = await client.getBooleanValue("my-flag", false, {
- targetingKey: "user-42",
-});
-```
-
----
-
-## Managing Flags via REST API
-
-All examples use `api.cloudflare.com`. Set `CLOUDFLARE_ACCOUNT_ID`, `FLAGSHIP_APP_ID`, and `CLOUDFLARE_API_TOKEN` first.
-
-### Create a Boolean Flag
-
-```bash
-curl -s -X POST \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d '{
- "key": "new-feature",
- "default_variation": "off",
- "variations": { "on": true, "off": false },
- "rules": [],
- "description": "Enable the new feature",
- "enabled": false
- }' \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags" | jq .
-```
-
-### Create a Flag with Internal-Only Targeting
-
-```bash
-curl -s -X POST \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d '{
- "key": "beta-feature",
- "default_variation": "off",
- "variations": { "on": true, "off": false },
- "rules": [
- {
- "priority": 1,
- "conditions": [
- { "attribute": "email", "operator": "ends_with", "value": "@cloudflare.com" }
- ],
- "serve_variation": "on"
- }
- ],
- "description": "Beta feature for internal users",
- "enabled": true
- }' \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags" | jq .
-```
-
-### Create a JSON Config Flag
-
-```bash
-curl -s -X POST \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d '{
- "key": "rate-limits",
- "default_variation": "standard",
- "variations": {
- "standard": { "rpm": 100, "burst": 20 },
- "premium": { "rpm": 1000, "burst": 200 }
- },
- "rules": [
- {
- "priority": 1,
- "conditions": [
- { "attribute": "plan", "operator": "in", "value": ["enterprise", "business"] }
- ],
- "serve_variation": "premium"
- }
- ],
- "description": "Rate limit configuration by plan",
- "enabled": true
- }' \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags" | jq .
-```
-
-### Read a Flag
-
-```bash
-curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/new-feature" | jq .
-```
-
-### List All Flags (with pagination)
-
-```bash
-curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags?limit=50" | jq .
-```
-
-If `result_info.cursor` is non-null, fetch the next page:
-
-```bash
-curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags?limit=50&cursor=" | jq .
-```
-
-### Update a Flag (Full Replace)
-
-Updates use PUT with the full `FlagDefinition`. Always GET first, modify, then PUT back.
-
-```bash
-# 1. Read current flag
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/new-feature" | jq '.result')
-
-# 2. Modify (e.g., enable the flag)
-UPDATED=$(echo "$FLAG" | jq '.enabled = true')
-
-# 3. PUT back
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/new-feature" | jq .
-```
-
-### Toggle a Flag On
-
-Read-modify-write to set `enabled: true`:
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.enabled = true')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/new-feature" | jq .
-```
-
-### Toggle a Flag Off (Disable)
-
-Same pattern, set `enabled: false`. The flag immediately returns its default variation for all evaluations.
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.enabled = false')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/new-feature" | jq .
-```
-
-### Add a Targeting Rule to an Existing Flag
-
-Append a rule to the existing rules array. Pick a priority that doesn't collide with existing rules.
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.rules += [{
- "priority": 2,
- "conditions": [{ "attribute": "plan", "operator": "equals", "value": "enterprise" }],
- "serve_variation": "on"
-}]')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/new-feature" | jq .
-```
-
-### Change Rollout Percentage
-
-Update the rollout percentage on an existing rule (e.g., rule at index 0):
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/gradual-rollout" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.rules[0].rollout.percentage = 50')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/gradual-rollout" | jq .
-```
-
-### Change Default Variation
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.default_variation = "on"')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/new-feature" | jq .
-```
-
-### Add a New Variation
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/checkout-flow" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.variations["treatment-c"] = "minimal"')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/checkout-flow" | jq .
-```
-
-### Remove a Rule
-
-Remove a rule by filtering on priority:
-
-```bash
-BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
-
-FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.result')
-UPDATED=$(echo "$FLAG" | jq '.rules = [.rules[] | select(.priority != 2)]')
-echo "$UPDATED" | curl -s -X PUT \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json" \
- -d @- "$BASE/new-feature" | jq .
-```
-
-### Delete a Flag
-
-```bash
-curl -s -X DELETE \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/old-feature" | jq .
-```
-
----
-
-## Targeting Rule Patterns
-
-### Enterprise-Only Access
-
-```json
-{
- "priority": 1,
- "conditions": [
- { "attribute": "plan", "operator": "equals", "value": "enterprise" }
- ],
- "serve_variation": "on"
-}
-```
-
-### Country-Based Targeting with Logical AND/OR
-
-Target enterprise users in the US or Canada:
-
-```json
-{
- "priority": 1,
- "conditions": [
- {
- "logical_operator": "AND",
- "clauses": [
- { "attribute": "plan", "operator": "equals", "value": "enterprise" },
- {
- "logical_operator": "OR",
- "clauses": [
- { "attribute": "country", "operator": "equals", "value": "US" },
- { "attribute": "country", "operator": "equals", "value": "CA" }
- ]
- }
- ]
- }
- ],
- "serve_variation": "on"
-}
-```
-
-### Percentage Rollout
-
-Gradually roll out to 10% of users:
-
-```json
-{
- "priority": 1,
- "conditions": [
- { "attribute": "targetingKey", "operator": "not_equals", "value": "" }
- ],
- "serve_variation": "on",
- "rollout": {
- "percentage": 10,
- "attribute": "targetingKey"
- }
-}
-```
-
-### A/B/n (Multi-Variant) Testing
-
-To split traffic across N variants, create one rule per variant with **cumulative** rollout percentages. Flagship evaluates rules in priority order. If a rule's conditions match but the user misses that rule's rollout percentage, evaluation continues to the next rule. Use the same stable rollout attribute on every rule so each user is compared against the same bucket as the thresholds increase.
-
-The example uses `conditions: []` because the rules are intended to match every context. For sticky user assignment, callers must still pass the configured bucketing attribute (`targetingKey` here); otherwise Flagship uses a random bucket per request.
-
-For example, to split traffic 30% / 40% / 30% across variants A, B, and C:
-
-| Variant | Share | Cumulative threshold |
-|---------|-------|----------------------|
-| A | 30% | 30 |
-| B | 40% | 70 |
-| C | 30% | 100 |
-
-```json
-"rules": [
- {
- "priority": 1,
- "conditions": [],
- "serve_variation": "variant-a",
- "rollout": { "percentage": 30, "attribute": "targetingKey" }
- },
- {
- "priority": 2,
- "conditions": [],
- "serve_variation": "variant-b",
- "rollout": { "percentage": 70, "attribute": "targetingKey" }
- },
- {
- "priority": 3,
- "conditions": [],
- "serve_variation": "variant-c",
- "rollout": { "percentage": 100, "attribute": "targetingKey" }
- }
-]
-```
-
-Key points:
-- Rules are evaluated lowest-priority-number first. A user who falls into rule 1's 0-30% bucket gets `variant-a` and is not evaluated further.
-- Rule 2's 70% threshold covers the next 40% of users (31-70%).
-- Rule 3's 100% threshold catches the remaining 30% (71-100%).
-- Always set the last rule to `100` so every context with the bucketing attribute is assigned a variant.
-- For sticky A/B/n assignment, pass a stable `targetingKey` or configured bucketing attribute. Without it, rollout assignment is random per request, which can be useful for request-level sampling but is usually wrong for user experiments.
-- A percentage rollout match reports reason `SPLIT` in evaluation details.
-
-### Progressive Rollout Workflow
-
-1. Create flag with 5% rollout, enable it
-2. Monitor metrics
-3. Increase to 25% → 50% → 100% by updating the `rollout.percentage`
-4. Once at 100%, remove the rule and set `default_variation` to the winning variation
-5. Eventually remove the flag and the code branch
-
----
-
-## Safe Deletion Workflow
-
-1. **Disable** the flag first (`enabled: false`) — confirms nothing depends on it being active
-2. **Monitor** for unexpected behavior
-3. **Remove** flag evaluation code from your application
-4. **Deploy** the code change
-5. **Delete** the flag via API
diff --git a/skills/cloudflare/references/graphql-api/README.md b/skills/cloudflare/references/graphql-api/README.md
deleted file mode 100644
index 3f92dd5..0000000
--- a/skills/cloudflare/references/graphql-api/README.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# Cloudflare GraphQL Analytics API
-
-Query analytics data across all Cloudflare products via a single GraphQL endpoint. Covers HTTP requests, Workers metrics, DNS, Firewall events, Network Analytics, and 70+ other datasets.
-
-## Overview
-
-- **Single endpoint** for all analytics: `https://api.cloudflare.com/client/v4/graphql`
-- **1,400+ schema types** spanning every Cloudflare product
-- **Two scopes**: zone-level (per-domain) and account-level (cross-domain)
-- **Adaptive sampling** on high-traffic datasets with confidence intervals
-- **No mutations** - read-only analytics (the Mutation type is a stub)
-- **Cost-based rate limiting** - default 300 queries per 5 minutes per user (max 320, varies by query cost)
-
-## Quick Decision Tree
-
-```
-Need analytics data from Cloudflare?
-├─ HTTP traffic (requests, bandwidth, cache) → httpRequestsAdaptiveGroups (zone or account)
-├─ Workers performance (CPU, wall time, errors) → workersInvocationsAdaptive (account)
-├─ Firewall/WAF events → firewallEventsAdaptive / firewallEventsAdaptiveGroups (zone or account)
-├─ DNS query analytics → dnsAnalyticsAdaptive / dnsAnalyticsAdaptiveGroups (zone or account)
-├─ Network layer (DDoS, Magic Transit) → *NetworkAnalyticsAdaptiveGroups (account)
-├─ Storage (R2, KV, D1, DO) → r2OperationsAdaptiveGroups / kvOperationsAdaptiveGroups / etc. (account)
-├─ AI (Workers AI, AI Gateway) → aiInferenceAdaptive / aiGatewayRequestsAdaptiveGroups (account)
-├─ Load Balancing → loadBalancingRequestsAdaptiveGroups (zone)
-├─ Custom high-cardinality metrics → Workers Analytics Engine (see ../analytics-engine/)
-└─ Need raw logs, not aggregates → Logpush (see Cloudflare docs)
-```
-
-## Core Concepts
-
-| Concept | Description |
-|---------|-------------|
-| **Endpoint** | `POST https://api.cloudflare.com/client/v4/graphql` |
-| **Explorer** | [graphql.cloudflare.com](https://graphql.cloudflare.com/) - interactive query builder |
-| **Viewer** | Root query object: `viewer { zones(...) { ... } }` or `viewer { accounts(...) { ... } }` |
-| **Dataset (Node)** | A queryable table under a zone or account (e.g., `httpRequestsAdaptiveGroups`) |
-| **Dimensions** | Fields to group by (time buckets, country, status code, script name, etc.) |
-| **Metrics** | Aggregation fields: `count`, `sum { ... }`, `avg { ... }`, `quantiles { ... }`, `ratio { ... }` |
-| **Filter** | Input object constraining results by time range, dimensions, etc. |
-| **Limit** | Maximum rows returned per dataset node (required, max varies by dataset) |
-| **OrderBy** | Enum-based sorting: `[field_ASC]` or `[field_DESC]` |
-| **Adaptive Sampling** | Nodes with `Adaptive` in the name use ABR sampling; results are statistically representative |
-
-## Query Structure
-
-Every query follows this pattern:
-
-```graphql
-{
- viewer {
- # Zone-scoped
- zones(filter: { zoneTag: "ZONE_ID" }) {
- datasetName(
- filter: { datetime_gt: "...", datetime_lt: "..." }
- limit: 1000
- orderBy: [datetimeFiveMinutes_DESC]
- ) {
- count
- dimensions { ... }
- sum { ... }
- }
- }
- # Account-scoped
- accounts(filter: { accountTag: "ACCOUNT_ID" }) {
- datasetName(filter: { ... }, limit: 100) {
- count
- dimensions { ... }
- sum { ... }
- }
- }
- }
-}
-```
-
-## Dataset Naming Convention
-
-Dataset names follow a consistent pattern visible in the schema:
-
-| Pattern | Meaning | Example |
-|---------|---------|---------|
-| `*Adaptive` | Raw rows with adaptive sampling; some (e.g., `workersInvocationsAdaptive`) also support aggregation fields (`sum`, `quantiles`, `avg`) | `httpRequestsAdaptive`, `workersInvocationsAdaptive` |
-| `*AdaptiveGroups` | Aggregated data with adaptive sampling | `httpRequestsAdaptiveGroups` |
-| `*1hGroups` | Hourly rollups (pre-aggregated) | `httpRequests1hGroups` |
-| `*1dGroups` | Daily rollups (pre-aggregated) | `httpRequests1dGroups` |
-| `*1mGroups` | Minutely rollups | `httpRequests1mGroups` |
-| `Zone*` prefix | Zone-scoped dataset | `ZoneHttpRequestsAdaptiveGroups` |
-| `Account*` prefix | Account-scoped dataset | `AccountWorkersInvocationsAdaptive` |
-
-**Prefer `*AdaptiveGroups` nodes** for most use cases - they support flexible time grouping via dimension fields (`datetimeFiveMinutes`, `datetimeHour`, etc.) and are the most commonly used.
-
-## Key Datasets by Product
-
-### Zone-Scoped (per-domain)
-
-| Dataset | Description |
-|---------|-------------|
-| `httpRequestsAdaptiveGroups` | HTTP traffic: requests, bytes, cache status, bot scores, WAF scores |
-| `httpRequests1hGroups` / `1dGroups` / `1mGroups` | Pre-aggregated HTTP rollups (hourly/daily/minutely) |
-| `firewallEventsAdaptiveGroups` | WAF, rate limiting, bot management, firewall rule events |
-| `dnsAnalyticsAdaptiveGroups` | DNS query volumes, response codes, query types |
-| `loadBalancingRequestsAdaptiveGroups` | Load Balancer origin request metrics |
-| `pageShieldReportsAdaptiveGroups` | Page Shield CSP reports |
-
-### Account-Scoped (cross-domain)
-
-| Dataset | Description |
-|---------|-------------|
-| `workersInvocationsAdaptive` | Workers: requests, errors, CPU time, wall time, subrequests |
-| `durableObjectsInvocationsAdaptiveGroups` | DO invocations |
-| `durableObjectsStorageGroups` / `durableObjectsPeriodicGroups` | DO storage and periodic metrics |
-| `d1AnalyticsAdaptiveGroups` / `d1QueriesAdaptiveGroups` | D1 database analytics |
-| `r2OperationsAdaptiveGroups` / `r2StorageAdaptiveGroups` | R2 operations and storage |
-| `kvOperationsAdaptiveGroups` / `kvStorageAdaptiveGroups` | KV operations and storage |
-| `aiInferenceAdaptiveGroups` | Workers AI inference metrics |
-| `aiGatewayRequestsAdaptiveGroups` | AI Gateway request analytics |
-| `pagesFunctionsInvocationsAdaptiveGroups` | Pages Functions metrics |
-| `magicTransitNetworkAnalyticsAdaptiveGroups` | Magic Transit packet/byte analytics |
-| `spectrumNetworkAnalyticsAdaptiveGroups` | Spectrum TCP/UDP analytics |
-| `gatewayL7RequestsAdaptiveGroups` | Zero Trust Gateway HTTP metrics |
-| `gatewayResolverQueriesAdaptiveGroups` | Zero Trust Gateway DNS metrics |
-
-## Reading Order
-
-| Task | Start Here | Then Read |
-|------|------------|-----------|
-| **First query** | [configuration.md](configuration.md) (auth) -> this README (structure) | [api.md](api.md) |
-| **Build a dashboard** | [patterns.md](patterns.md) (time-series, top-N) | [api.md](api.md) (aggregation fields) |
-| **Debug query issues** | [gotchas.md](gotchas.md) | [api.md](api.md) (filtering) |
-| **Understand sampling** | [gotchas.md](gotchas.md) (sampling section) | [api.md](api.md) (confidence intervals) |
-| **Product-specific metrics** | [patterns.md](patterns.md) (per-product examples) | [api.md](api.md) (dataset reference) |
-
-## In This Reference
-
-- **[api.md](api.md)** - Query structure, aggregation fields (sum/avg/quantiles/count), filtering operators, dimensions, dataset details
-- **[configuration.md](configuration.md)** - Authentication, API tokens, client setup (curl, JS, Python), introspection
-- **[patterns.md](patterns.md)** - Common queries: time-series, top-N, Workers metrics, HTTP analytics, firewall events, multi-zone
-- **[gotchas.md](gotchas.md)** - Rate limits, sampling caveats, query cost, common errors, plan-based limits
-
-## See Also
-
-- [GraphQL Analytics API Docs](https://developers.cloudflare.com/analytics/graphql-api/)
-- [GraphQL API Explorer](https://graphql.cloudflare.com/)
-- [Observability Reference](../observability/) - Workers Logs, Tail Workers, console logging
-- [Analytics Engine Reference](../analytics-engine/) - Custom high-cardinality analytics via Workers
-- [Web Analytics Reference](../web-analytics/) - Client-side (RUM) analytics
-- [API Reference](../api/) - REST API, SDKs, authentication basics
diff --git a/skills/cloudflare/references/graphql-api/api.md b/skills/cloudflare/references/graphql-api/api.md
deleted file mode 100644
index 15d1a33..0000000
--- a/skills/cloudflare/references/graphql-api/api.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# GraphQL Analytics API Reference
-
-## Query Root
-
-The schema has a single entry point: `Query.viewer`. Mutations are not supported.
-
-```graphql
-{
- cost # uint64 -- query cost (returned in response)
- viewer {
- budget # uint64 -- remaining budget
- zones(filter: { zoneTag: "..." }) { ... }
- accounts(filter: { accountTag: "..." }) { ... }
- }
-}
-```
-
-## Aggregation Fields
-
-Aggregated dataset nodes (`*Groups`) return these field categories. Not every node has all — use introspection to check.
-
-### count
-
-Total events in the group. Available on `*Groups` nodes but **not** on raw `*Adaptive` nodes (e.g., `workersInvocationsAdaptive` — use `sum { requests }` instead).
-
-### sum
-
-Cumulative metrics. Fields vary by dataset:
-
-```graphql
-# HTTP requests
-sum { edgeResponseBytes edgeRequestBytes visits edgeTimeToFirstByteMs originResponseDurationMs }
-
-# Workers invocations
-sum { requests errors subrequests cpuTimeUs wallTime duration responseBodySize clientDisconnects requestDuration }
-```
-
-### quantiles
-
-Percentile distributions (on datasets like `workersInvocationsAdaptive`). Available percentiles: P25, P50, P75, P90, P95, P99, P999 for `cpuTime`, `wallTime`, `requestDuration`, `duration`, `responseBodySize`.
-
-```graphql
-quantiles { cpuTimeP50 cpuTimeP99 wallTimeP50 wallTimeP99 }
-```
-
-### ratio, avg, uniq, confidence
-
-```graphql
-ratio { status4xx status5xx } # float64 (0 to 1) -- HTTP datasets only
-avg { sampleInterval } # useful for understanding sampling resolution
-uniq { uniques } # unique IP count -- rollup datasets (*1hGroups, *1dGroups) only
-confidence(level: 0.95) { # Adaptive datasets only; works on count and sum fields
- count { estimate lower upper sampleSize }
-}
-```
-
-## Dimensions
-
-Dimensions are fields you can group by via the `dimensions` sub-selection.
-
-### Time Dimensions
-
-| Dimension | Granularity |
-|-----------|------------|
-| `date` | Day |
-| `datetime` | Exact timestamp |
-| `datetimeMinute` | 1 minute |
-| `datetimeFiveMinutes` | 5 minutes |
-| `datetimeFifteenMinutes` | 15 minutes |
-| `datetimeHour` | 1 hour |
-
-Workers datasets also support `datetimeSixHours`.
-
-### HTTP Request Dimensions (httpRequestsAdaptiveGroups)
-
-83 dimensions available. Key ones:
-
-| Dimension | Description |
-|-----------|-------------|
-| `clientCountryName` | Country of origin |
-| `clientRequestHTTPHost` | Requested hostname |
-| `clientRequestHTTPMethodName` | HTTP method |
-| `clientRequestPath` | URI path |
-| `edgeResponseStatus` | Edge HTTP status code |
-| `cacheStatus` | Cache status (hit, miss, dynamic, etc.) |
-| `coloCode` | Cloudflare datacenter IATA code |
-| `clientIP` / `clientAsn` | Client IP address / ASN |
-| `botScore` / `botManagementDecision` | Bot management score (0-99) / verdict |
-| `wafAttackScore` / `securityAction` | WAF score / firewall action taken |
-| `ja3Hash` / `ja4` | TLS fingerprints |
-| `sampleInterval` | ABR sample interval |
-
-### Workers Dimensions (workersInvocationsAdaptive)
-
-`scriptName`, `scriptTag`, `scriptVersion`, `environmentName`, `status`, `usageModel`, `coloCode`, `dispatchNamespaceName`, `isDispatcher`
-
-### Firewall Dimensions (firewallEventsAdaptive)
-
-`action`, `source`, `ruleId`, `clientCountryName`, `clientIP`, `clientAsn`, `userAgent`
-
-## Filtering
-
-### Scope Filters
-
-```graphql
-zones(filter: { zoneTag: "ZONE_ID" }) # up to 10 zones
-zones(filter: { zoneTag_in: ["Z1", "Z2"] })
-accounts(filter: { accountTag: "ACCOUNT_ID" }) # exactly 1 account
-```
-
-### Dataset Filters
-
-**Always include a time range filter.** Multiple filters at the same level are implicitly AND-ed.
-
-```graphql
-httpRequestsAdaptiveGroups(
- filter: { datetime_gt: "2025-01-01T00:00:00Z", datetime_lt: "2025-01-02T00:00:00Z", clientCountryName: "US" }
- limit: 1000
-)
-```
-
-### Filter Operators
-
-| Operator | Meaning | Example |
-|----------|---------|---------|
-| (none) | equals | `clientCountryName: "US"` |
-| `_gt` / `_lt` | greater / less than | `datetime_gt: "..."` |
-| `_geq` / `_leq` | greater/less or equal | `datetime_geq: "..."` |
-| `_neq` | not equal | `cacheStatus_neq: "hit"` |
-| `_in` / `_notin` | in / not in list | `clientCountryName_in: ["US", "GB"]` |
-| `_like` / `_notlike` | SQL LIKE with `%` | `clientRequestPath_like: "/api/%"` |
-| `_has` / `_hasall` / `_hasany` | array contains | `botDetectionIds_has: "abc"` |
-
-> `_notin` and `_notlike` are in the schema but not in official docs. Confirmed via introspection.
-
-### Boolean Operators (AND / OR)
-
-```graphql
-# Explicit AND
-filter: { AND: [{ datetime_gt: "..." }, { datetime_lt: "..." }, { clientCountryName: "US" }] }
-
-# Explicit OR
-filter: { datetime_gt: "...", OR: [{ edgeResponseStatus: 403 }, { edgeResponseStatus: 429 }] }
-```
-
-## Pagination & Sorting
-
-No cursor-based pagination. Use `limit`, `orderBy`, and filter-based offsets:
-
-```graphql
-# First page
-httpRequestsAdaptiveGroups(filter: { datetime_gt: "..." }, limit: 100, orderBy: [datetime_ASC])
-
-# Next page: filter by last seen value from previous page
-httpRequestsAdaptiveGroups(filter: { datetime_gt: "2025-01-01T01:35:00Z" }, limit: 100, orderBy: [datetime_ASC])
-```
-
-Sort with `orderBy: [field_ASC]` or `[field_DESC]`. Multiple sort fields supported.
-
-## Settings Node
-
-Query per-node limits and availability:
-
-```graphql
-viewer { zones(filter: { zoneTag: "..." }) { settings {
- httpRequestsAdaptiveGroups { enabled maxDuration maxNumberOfFields maxPageSize notOlderThan }
-} } }
-```
-
-## See Also
-
-- [README.md](README.md) - Overview, decision tree, dataset index
-- [configuration.md](configuration.md) - Authentication, client setup, introspection queries
-- [patterns.md](patterns.md) - Common query patterns (time-series, top-N, per-product)
-- [gotchas.md](gotchas.md) - Rate limits, sampling, troubleshooting
diff --git a/skills/cloudflare/references/graphql-api/configuration.md b/skills/cloudflare/references/graphql-api/configuration.md
deleted file mode 100644
index d85d1a6..0000000
--- a/skills/cloudflare/references/graphql-api/configuration.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# GraphQL Analytics API Configuration
-
-## Authentication
-
-### API Token (Recommended)
-
-| Permission | Scope | Use Case |
-|------------|-------|----------|
-| **Account Analytics: Read** | Account-wide | Workers, R2, KV, D1, DO, AI, Network Analytics |
-| **Zone Analytics: Read** | Per-zone | HTTP requests, Firewall, DNS, Load Balancing |
-| **All zones - Analytics: Read** | All zones | Multi-zone HTTP/Firewall/DNS queries |
-
-Create tokens at: [dash.cloudflare.com > Account API Tokens](https://dash.cloudflare.com/?to=/:account/api-tokens)
-
-```bash
-# Verify token
-curl -s https://api.cloudflare.com/client/v4/graphql \
- -H "Authorization: Bearer $CF_API_TOKEN" \
- -H "Content-Type: application/json" \
- --data '{"query":"{ viewer { zones(filter: {zoneTag: \"ZONE_ID\"}) { httpRequestsAdaptiveGroups(limit: 1, filter: {datetime_gt: \"2025-01-01T00:00:00Z\"}) { count } } } }"}'
-```
-
-### API Key + Email (Legacy)
-
-Not recommended. Use `X-Auth-Email` + `X-Auth-Key` headers instead of `Authorization: Bearer`.
-
-## Client Setup
-
-### curl
-
-```bash
-curl -s https://api.cloudflare.com/client/v4/graphql \
- -H "Authorization: Bearer $CF_API_TOKEN" \
- -H "Content-Type: application/json" \
- --data '{
- "query": "query($zoneTag: string!, $start: Time!, $end: Time!) { viewer { zones(filter: {zoneTag: $zoneTag}) { httpRequestsAdaptiveGroups(filter: {datetime_gt: $start, datetime_lt: $end}, limit: 10, orderBy: [datetimeFiveMinutes_DESC]) { count dimensions { datetimeFiveMinutes } } } } }",
- "variables": { "zoneTag": "ZONE_ID", "start": "2025-01-01T00:00:00Z", "end": "2025-01-02T00:00:00Z" }
- }' | jq .
-```
-
-### TypeScript / JavaScript
-
-```typescript
-const GRAPHQL_ENDPOINT = "https://api.cloudflare.com/client/v4/graphql";
-
-async function queryGraphQL(query: string, variables: Record = {}): Promise {
- const response = await fetch(GRAPHQL_ENDPOINT, {
- method: "POST",
- headers: { Authorization: `Bearer ${process.env.CF_API_TOKEN}`, "Content-Type": "application/json" },
- body: JSON.stringify({ query, variables }),
- });
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- const json = await response.json() as { data: T | null; errors?: { message: string }[] };
- if (json.errors?.length) throw new Error(json.errors.map((e) => e.message).join("; "));
- return json.data!;
-}
-```
-
-### Python
-
-```python
-import requests, os
-
-def query_graphql(query: str, variables: dict = None) -> dict:
- r = requests.post("https://api.cloudflare.com/client/v4/graphql",
- headers={"Authorization": f"Bearer {os.environ['CF_API_TOKEN']}", "Content-Type": "application/json"},
- json={"query": query, "variables": variables or {}})
- r.raise_for_status()
- result = r.json()
- if result.get("errors"):
- raise Exception("; ".join(e["message"] for e in result["errors"]))
- return result["data"]
-```
-
-### From a Cloudflare Worker
-
-Store the API token as a secret (`CF_API_TOKEN`). Use standard `fetch` to POST to `https://api.cloudflare.com/client/v4/graphql` with the same JSON body format as above. Always check `response.errors` — GraphQL returns 200 even on query failures.
-
-## GraphQL API Explorer
-
-Interactive explorer at [graphql.cloudflare.com](https://graphql.cloudflare.com/) — provides schema docs, autocomplete, variable panel, and shareable queries. Authenticates via your Cloudflare dashboard session.
-
-## Schema Introspection
-
-```graphql
-# List zone-scoped datasets
-{ __type(name: "zone") { fields { name description } } }
-
-# List account-scoped datasets
-{ __type(name: "account") { fields { name description } } }
-
-# Discover dimensions for a dataset
-{ __type(name: "ZoneHttpRequestsAdaptiveGroupsDimensions") {
- fields { name type { name kind } }
-} }
-
-# Discover filter operators for a dataset
-{ __type(name: "ZoneHttpRequestsAdaptiveGroupsFilter_InputObject") {
- inputFields { name type { name kind } }
-} }
-```
-
-## Finding Your Zone and Account IDs
-
-- **Zone ID**: Dashboard > select zone > Overview (right sidebar), or via API
-- **Account ID**: Dashboard > Account Home URL, or via API
-
-```bash
-curl -s https://api.cloudflare.com/client/v4/zones -H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id}'
-curl -s https://api.cloudflare.com/client/v4/accounts -H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id}'
-```
-
-## See Also
-
-- [README.md](README.md) - Overview, decision tree, dataset index
-- [api.md](api.md) - Query structure, aggregation fields, filtering operators
-- [patterns.md](patterns.md) - Common query patterns (time-series, top-N, per-product)
-- [gotchas.md](gotchas.md) - Rate limits, sampling, troubleshooting
diff --git a/skills/cloudflare/references/graphql-api/gotchas.md b/skills/cloudflare/references/graphql-api/gotchas.md
deleted file mode 100644
index 18df7c6..0000000
--- a/skills/cloudflare/references/graphql-api/gotchas.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# GraphQL Analytics API Gotchas & Troubleshooting
-
-## Rate Limits
-
-| Limit | Value |
-|-------|-------|
-| GraphQL queries per user | **Default 300 per 5 minutes** (max 320, at least 1/sec) |
-| General API rate limit | 1200 requests per 5 minutes (shared across all API calls) |
-| Zone scope per query | Up to **10 zones** |
-| Account scope per query | Exactly **1 account** |
-
-The GraphQL rate limit is separate from the general API limit. Exceeding either results in `HTTP 429` and blocks all API calls for 5 minutes. Enterprise customers can contact support to raise limits.
-
-### "429 Too Many Requests"
-
-**Cause:** Exceeded rate limit.
-
-**Solution:** Batch multiple datasets into single queries, cache results, increase intervals between queries. Use `{ viewer { budget } }` to monitor remaining budget.
-
-## Sampling & Data Accuracy
-
-### Adaptive Bit Rate (ABR) Sampling
-
-Datasets with `Adaptive` in the name use adaptive sampling:
-- Results are **statistically representative**, not exact
-- Same query may return **slightly different numbers** each run
-- Higher traffic = higher sampling rate = more accurate
-- `sampleInterval` dimension shows the ratio (1 = no sampling, 10 = ~1-in-10 sampled)
-
-For high-confidence numbers, use `confidence(level: 0.95)` to get estimate bounds. For exact counts, use rollup nodes (`httpRequests1hGroups`, `httpRequests1dGroups`) which are pre-aggregated without sampling.
-
-### Rollup vs. Adaptive
-
-| Feature | Rollup (`*1hGroups`, `*1dGroups`) | Adaptive (`*AdaptiveGroups`) |
-|---------|-----------------------------------|-----------------------------|
-| Sampling | No (pre-aggregated) | Yes (ABR) |
-| Flexibility | Fixed time buckets | Any granularity |
-| Dimensions | Fewer | Many more |
-| Accuracy | Exact | Statistical estimate |
-
-## Common Errors
-
-### "Access denied" / "authentication error"
-
-**Cause:** Token lacks required permission or wrong scope.
-
-**Solution:** Account-scoped queries need **Account Analytics: Read**. Zone-scoped queries need **Zone Analytics: Read**. Verify: `curl -s https://api.cloudflare.com/client/v4/user/tokens/verify -H "Authorization: Bearer $TOKEN"`
-
-### "field not found" / "Cannot query field"
-
-**Cause:** Wrong dataset name, nonexistent field, or wrong scope (zone vs. account).
-
-**Solution:** Names are case-sensitive camelCase (`httpRequestsAdaptiveGroups`). Zone datasets go under `zones(...)`, account datasets under `accounts(...)`. Use introspection to verify.
-
-### "filter is required" / empty results
-
-**Cause:** Missing required time range filter or incorrect zone/account tag.
-
-**Solution:** Always include `datetime_gt` / `datetime_lt` (or `_geq` / `_leq`).
-
-### "limit is required" / "limit exceeds maximum"
-
-**Cause:** Missing `limit` or exceeding node's max page size.
-
-**Solution:** Always specify `limit`. Max varies by dataset (typically 10,000 for groups, 100 for raw events). Check via settings query.
-
-### "query is too complex" / "query exceeds budget"
-
-**Cause:** Too many fields, datasets, or too broad a time range.
-
-**Solution:** Reduce time range, request fewer dimensions/metrics, break into smaller queries. Monitor `cost` and `budget` in responses.
-
-### 200 Response with Errors
-
-GraphQL returns HTTP 200 even on failures. **Always check `response.errors`:**
-
-```json
-{ "data": null, "errors": [{ "message": "filter is required for httpRequestsAdaptiveGroups" }] }
-```
-
-## Plan-Based Availability
-
-Not all datasets are available on all plans. Higher plans get more datasets, longer retention (`notOlderThan`), wider time ranges (`maxDuration`), more fields, and larger page sizes.
-
-### "node is not available" / "node is disabled"
-
-**Cause:** Dataset not on your plan, or product not enabled.
-
-**Solution:** Check `settings { { enabled } }`. Some datasets require specific subscriptions (e.g., Network Analytics requires Magic Transit/Spectrum).
-
-## DateTime & Timezone Handling
-
-- All times are **UTC only** (ISO 8601: `"2025-01-15T10:30:00Z"`)
-- `Date` type: `"2025-01-15"` (used in `date_geq`/`date_leq` for storage datasets)
-- `Time` type: `"2025-01-15T10:30:00Z"` (used in `datetime_gt`/`datetime_lt`)
-- Filters are start-inclusive: events that start within the window are included
-
-## Performance Tips
-
-- **Narrow time ranges** are faster and cheaper
-- **Select only needed dimensions** — each additional dimension increases cost
-- **Use rollup nodes** (`*1dGroups`) for simple daily totals without dimension breakdowns
-- **Batch datasets** into one query instead of separate HTTP requests
-
-## See Also
-
-- [README.md](README.md) - Overview, decision tree, dataset index
-- [api.md](api.md) - Query structure, aggregation fields, filtering operators
-- [configuration.md](configuration.md) - Authentication, client setup, introspection queries
-- [patterns.md](patterns.md) - Common query patterns (time-series, top-N, per-product)
diff --git a/skills/cloudflare/references/graphql-api/patterns.md b/skills/cloudflare/references/graphql-api/patterns.md
deleted file mode 100644
index f02c36b..0000000
--- a/skills/cloudflare/references/graphql-api/patterns.md
+++ /dev/null
@@ -1,225 +0,0 @@
-# GraphQL Analytics API Patterns & Best Practices
-
-## Time-Series Queries
-
-Use time dimension granularity matching your range (see Best Practices below).
-
-```graphql
-query TrafficTimeSeries($zoneTag: string!, $start: Time!, $end: Time!) {
- viewer {
- zones(filter: { zoneTag: $zoneTag }) {
- httpRequestsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }
- limit: 1000
- orderBy: [datetimeFiveMinutes_ASC] # or datetimeHour_ASC for longer ranges
- ) {
- count
- dimensions { datetimeFiveMinutes }
- sum { edgeResponseBytes }
- ratio { status4xx status5xx }
- }
- }
- }
-}
-```
-
-## Top-N Queries
-
-### Top Countries by Request Count
-
-```graphql
-query TopCountries($zoneTag: string!, $start: Time!, $end: Time!) {
- viewer {
- zones(filter: { zoneTag: $zoneTag }) {
- httpRequestsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }
- limit: 10
- orderBy: [count_DESC]
- ) {
- count
- dimensions { clientCountryName }
- }
- }
- }
-}
-```
-
-Use `orderBy: [sum_edgeResponseBytes_DESC]` for top paths by bandwidth. Add `edgeResponseStatus_geq: 400` to the filter for top error status codes.
-
-## Workers Analytics
-
-```graphql
-query WorkersOverview($accountTag: string!, $start: Time!, $end: Time!) {
- viewer {
- accounts(filter: { accountTag: $accountTag }) {
- workersInvocationsAdaptive(
- filter: { datetime_gt: $start, datetime_lt: $end }
- limit: 100
- orderBy: [sum_requests_DESC]
- ) {
- sum { requests errors subrequests wallTime }
- quantiles { cpuTimeP50 cpuTimeP99 wallTimeP50 wallTimeP99 }
- dimensions { scriptName }
- }
- }
- }
-}
-```
-
-Filter by `scriptName` for a specific Worker. Add `datetimeFiveMinutes` dimension + `orderBy: [datetimeFiveMinutes_ASC]` for error rate over time.
-
-## Firewall / Security
-
-```graphql
-query RecentFirewallEvents($zoneTag: string!, $start: Time!) {
- viewer {
- zones(filter: { zoneTag: $zoneTag }) {
- firewallEventsAdaptive(
- filter: { datetime_gt: $start }
- limit: 50
- orderBy: [datetime_DESC]
- ) {
- action source clientIP clientCountryName userAgent
- clientRequestHTTPHost clientRequestPath ruleId datetime
- }
- }
- }
-}
-```
-
-For aggregated firewall stats, use `firewallEventsAdaptiveGroups` with `action: "block"` filter and group by `ruleId`, `source`, `datetimeHour`.
-
-## DNS Analytics
-
-```graphql
-query DNSQueryVolume($zoneTag: string!, $start: Time!, $end: Time!) {
- viewer {
- zones(filter: { zoneTag: $zoneTag }) {
- dnsAnalyticsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }
- limit: 500
- orderBy: [datetimeFiveMinutes_ASC]
- ) {
- count
- dimensions { datetimeFiveMinutes }
- }
- }
- }
-}
-```
-
-## Storage Analytics (Account-Scoped)
-
-R2, KV, and D1 use `date` (Date type) filters instead of `datetime` (Time type).
-
-```graphql
-# R2 operations
-r2OperationsAdaptiveGroups(filter: { date_geq: $start, date_leq: $end }, limit: 100, orderBy: [date_DESC]) {
- dimensions { date bucketName actionType }
- sum { requests }
-}
-
-# KV operations
-kvOperationsAdaptiveGroups(filter: { date_geq: $start, date_leq: $end }, limit: 100, orderBy: [date_DESC]) {
- dimensions { date actionType }
- sum { requests }
-}
-
-# D1 analytics
-d1AnalyticsAdaptiveGroups(filter: { date_geq: $start, date_leq: $end }, limit: 100, orderBy: [date_DESC]) {
- dimensions { date databaseId }
- sum { readQueries writeQueries rowsRead rowsWritten }
-}
-```
-
-## Cache Analytics
-
-```graphql
-query CacheStatusBreakdown($zoneTag: string!, $start: Time!, $end: Time!) {
- viewer {
- zones(filter: { zoneTag: $zoneTag }) {
- httpRequestsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }
- limit: 20
- orderBy: [count_DESC]
- ) {
- count
- dimensions { cacheStatus }
- sum { edgeResponseBytes }
- }
- }
- }
-}
-```
-
-For cache hit ratio over time, use aliases to query the same dataset twice — once with `cacheStatus: "hit"` filter and once without — then compute the ratio client-side.
-
-## Multi-Dataset Queries
-
-A single request can query multiple datasets, avoiding extra HTTP round-trips:
-
-```graphql
-query DashboardOverview($zoneTag: string!, $start: Time!, $end: Time!) {
- viewer {
- zones(filter: { zoneTag: $zoneTag }) {
- httpTraffic: httpRequestsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }, limit: 1
- ) { count sum { edgeResponseBytes } ratio { status4xx status5xx } }
- firewallEvents: firewallEventsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }, limit: 5, orderBy: [count_DESC]
- ) { count dimensions { action source } }
- dnsQueries: dnsAnalyticsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }, limit: 1
- ) { count }
- }
- }
-}
-```
-
-## AI & Gateway Analytics
-
-```graphql
-# Workers AI inference
-aiInferenceAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }, limit: 100, orderBy: [datetimeHour_DESC]
-) {
- count
- sum { totalInputTokens totalOutputTokens totalRequestBytesIn }
- dimensions { modelId datetimeHour }
-}
-
-# AI Gateway requests
-aiGatewayRequestsAdaptiveGroups(
- filter: { datetime_gt: $start, datetime_lt: $end }, limit: 100, orderBy: [datetimeHour_DESC]
-) {
- count
- dimensions { gateway provider model datetimeHour }
- sum { cachedTokensIn cachedTokensOut uncachedTokensIn uncachedTokensOut }
-}
-```
-
-Both are account-scoped — nest under `accounts(filter: { accountTag: $accountTag })`.
-
-## Best Practices
-
-**Always include time filters.** Queries without time filters scan all data and are slow/expensive.
-
-**Match time granularity to range:**
-
-| Time Range | Recommended Dimension |
-|------------|----------------------|
-| < 6 hours | `datetimeMinute` or `datetimeFiveMinutes` |
-| 6-48 hours | `datetimeFiveMinutes` or `datetimeFifteenMinutes` |
-| 2-14 days | `datetimeHour` |
-| 14+ days | `date` |
-
-**Use aliases** for querying the same dataset with different filters in one request.
-
-**Request only needed fields.** Extra dimensions and metrics increase query cost.
-
-## See Also
-
-- [README.md](README.md) - Overview, decision tree, dataset index
-- [api.md](api.md) - Query structure, aggregation fields, filtering operators
-- [configuration.md](configuration.md) - Authentication, client setup, introspection queries
-- [gotchas.md](gotchas.md) - Rate limits, sampling, troubleshooting
diff --git a/skills/cloudflare/references/hyperdrive/README.md b/skills/cloudflare/references/hyperdrive/README.md
deleted file mode 100644
index 6626776..0000000
--- a/skills/cloudflare/references/hyperdrive/README.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# Hyperdrive
-
-Accelerates database queries from Workers via connection pooling, edge setup, query caching.
-
-## Key Features
-
-- **Connection Pooling**: Persistent connections eliminate TCP/TLS/auth handshakes (~7 round-trips)
-- **Edge Setup**: Connection negotiation at edge, pooling near origin
-- **Query Caching**: Auto-cache non-mutating queries (default 60s TTL)
-- **Support**: PostgreSQL, MySQL + compatibles (CockroachDB, Timescale, PlanetScale, Neon, Supabase)
-
-## Architecture
-
-```
-Worker → Edge (setup) → Pool (near DB) → Origin
- ↓ cached reads
- Cache
-```
-
-## Quick Start
-
-```bash
-# Create config
-npx wrangler hyperdrive create my-db \
- --connection-string="postgres://user:pass@host:5432/db"
-
-# wrangler.jsonc
-{
- "compatibility_flags": ["nodejs_compat"],
- "hyperdrive": [{"binding": "HYPERDRIVE", "id": ""}]
-}
-```
-
-```typescript
-import { Client } from "pg";
-
-export default {
- async fetch(req: Request, env: Env): Promise {
- const client = new Client({
- connectionString: env.HYPERDRIVE.connectionString,
- });
- await client.connect();
- const result = await client.query("SELECT * FROM users WHERE id = $1", [123]);
- await client.end();
- return Response.json(result.rows);
- },
-};
-```
-
-## When to Use
-
-✅ Global access to single-region DBs, high read ratios, popular queries, connection-heavy loads
-❌ Write-heavy, real-time data (<1s), single-region apps close to DB
-
-**💡 Pair with Smart Placement** for Workers making multiple queries - executes near DB to minimize latency.
-
-## Driver Choice
-
-| Driver | Use When | Notes |
-|--------|----------|-------|
-| **pg** (recommended) | General use, TypeScript, ecosystem compatibility | Stable, widely used, works with most ORMs |
-| **postgres.js** | Advanced features, template literals, streaming | Lighter than pg, `prepare: true` is default |
-| **mysql2** | MySQL/MariaDB/PlanetScale | MySQL only, less mature support |
-
-## Reading Order
-
-| New to Hyperdrive | Implementing | Troubleshooting |
-|-------------------|--------------|-----------------|
-| 1. README (this) | 1. [configuration.md](./configuration.md) | 1. [gotchas.md](./gotchas.md) |
-| 2. [configuration.md](./configuration.md) | 2. [api.md](./api.md) | 2. [patterns.md](./patterns.md) |
-| 3. [api.md](./api.md) | 3. [patterns.md](./patterns.md) | 3. [api.md](./api.md) |
-
-## In This Reference
-- [configuration.md](./configuration.md) - Setup, wrangler config, Smart Placement
-- [api.md](./api.md) - Binding APIs, query patterns, driver usage
-- [patterns.md](./patterns.md) - Use cases, ORMs, multi-query optimization
-- [gotchas.md](./gotchas.md) - Limits, troubleshooting, connection management
-
-## See Also
-- [smart-placement](../smart-placement/) - Optimize multi-query Workers near databases
-- [d1](../d1/) - Serverless SQLite alternative for edge-native apps
-- [workers](../workers/) - Worker runtime with database bindings
diff --git a/skills/cloudflare/references/hyperdrive/api.md b/skills/cloudflare/references/hyperdrive/api.md
deleted file mode 100644
index 0e587b9..0000000
--- a/skills/cloudflare/references/hyperdrive/api.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# API Reference
-
-See [README.md](./README.md) for overview, [configuration.md](./configuration.md) for setup.
-
-## Binding Interface
-
-```typescript
-interface Hyperdrive {
- connectionString: string; // PostgreSQL
- // MySQL properties:
- host: string;
- port: number;
- user: string;
- password: string;
- database: string;
-}
-
-interface Env {
- HYPERDRIVE: Hyperdrive;
-}
-```
-
-**Generate types:** `npx wrangler types` (auto-creates worker-configuration.d.ts from wrangler.jsonc)
-
-## PostgreSQL (node-postgres) - RECOMMENDED
-
-```typescript
-import { Client } from "pg"; // pg@^8.17.2
-
-export default {
- async fetch(req: Request, env: Env): Promise {
- const client = new Client({connectionString: env.HYPERDRIVE.connectionString});
- try {
- await client.connect();
- const result = await client.query("SELECT * FROM users WHERE id = $1", [123]);
- return Response.json(result.rows);
- } finally {
- await client.end();
- }
- },
-};
-```
-
-**⚠️ Workers connection limit: 6 per Worker invocation** - use connection pooling wisely.
-
-## PostgreSQL (postgres.js)
-
-```typescript
-import postgres from "postgres"; // postgres@^3.4.8
-
-const sql = postgres(env.HYPERDRIVE.connectionString, {
- max: 5, // Limit per Worker (Workers max: 6)
- prepare: true, // Enabled by default, required for caching
- fetch_types: false, // Reduce latency if not using arrays
-});
-
-const users = await sql`SELECT * FROM users WHERE active = ${true} LIMIT 10`;
-```
-
-**⚠️ `prepare: true` is enabled by default and required for Hyperdrive caching.** Setting to `false` disables prepared statements + cache.
-
-## MySQL (mysql2)
-
-```typescript
-import { createConnection } from "mysql2/promise"; // mysql2@^3.16.2
-
-const conn = await createConnection({
- host: env.HYPERDRIVE.host,
- user: env.HYPERDRIVE.user,
- password: env.HYPERDRIVE.password,
- database: env.HYPERDRIVE.database,
- port: env.HYPERDRIVE.port,
- disableEval: true, // ⚠️ REQUIRED for Workers
-});
-
-const [results] = await conn.query("SELECT * FROM users WHERE active = ? LIMIT ?", [true, 10]);
-ctx.waitUntil(conn.end());
-```
-
-**⚠️ MySQL support is less mature than PostgreSQL** - expect fewer optimizations and potential edge cases.
-
-## Query Caching
-
-**Cacheable:**
-```sql
-SELECT * FROM posts WHERE published = true;
-SELECT COUNT(*) FROM users;
-```
-
-**NOT cacheable:**
-```sql
--- Writes
-INSERT/UPDATE/DELETE
-
--- Volatile functions
-SELECT NOW();
-SELECT random();
-SELECT LASTVAL(); -- PostgreSQL
-SELECT UUID(); -- MySQL
-```
-
-**Cache config:**
-- Default: `max_age=60s`, `swr=15s`
-- Max `max_age`: 3600s
-- Disable: `--caching-disabled=true`
-
-**Multiple configs pattern:**
-```typescript
-// Reads: cached
-const sqlCached = postgres(env.HYPERDRIVE_CACHED.connectionString);
-const posts = await sqlCached`SELECT * FROM posts ORDER BY views DESC LIMIT 10`;
-
-// Writes/time-sensitive: no cache
-const sqlNoCache = postgres(env.HYPERDRIVE_NO_CACHE.connectionString);
-const orders = await sqlNoCache`SELECT * FROM orders WHERE created_at > NOW() - INTERVAL 5 MINUTE`;
-```
-
-## ORMs
-
-**Drizzle:**
-```typescript
-import { drizzle } from "drizzle-orm/postgres-js"; // drizzle-orm@^0.45.1
-import postgres from "postgres";
-
-const client = postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true});
-const db = drizzle(client);
-const users = await db.select().from(users).where(eq(users.active, true)).limit(10);
-```
-
-**Kysely:**
-```typescript
-import { Kysely, PostgresDialect } from "kysely"; // kysely@^0.27+
-import postgres from "postgres";
-
-const db = new Kysely({
- dialect: new PostgresDialect({
- postgres: postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true}),
- }),
-});
-const users = await db.selectFrom("users").selectAll().where("active", "=", true).execute();
-```
-
-See [patterns.md](./patterns.md) for use cases, [gotchas.md](./gotchas.md) for limits.
diff --git a/skills/cloudflare/references/hyperdrive/configuration.md b/skills/cloudflare/references/hyperdrive/configuration.md
deleted file mode 100644
index 6d429a9..0000000
--- a/skills/cloudflare/references/hyperdrive/configuration.md
+++ /dev/null
@@ -1,159 +0,0 @@
-# Configuration
-
-See [README.md](./README.md) for overview.
-
-## Create Config
-
-**PostgreSQL:**
-```bash
-# Basic
-npx wrangler hyperdrive create my-db \
- --connection-string="postgres://user:pass@host:5432/db"
-
-# Custom cache
-npx wrangler hyperdrive create my-db \
- --connection-string="postgres://..." \
- --max-age=120 --swr=30
-
-# No cache
-npx wrangler hyperdrive create my-db \
- --connection-string="postgres://..." \
- --caching-disabled=true
-```
-
-**MySQL:**
-```bash
-npx wrangler hyperdrive create my-db \
- --connection-string="mysql://user:pass@host:3306/db"
-```
-
-## wrangler.jsonc
-
-```jsonc
-{
- "compatibility_date": "2025-01-01", // Use latest for new projects
- "compatibility_flags": ["nodejs_compat"],
- "hyperdrive": [
- {
- "binding": "HYPERDRIVE",
- "id": "",
- "localConnectionString": "postgres://user:pass@localhost:5432/dev"
- }
- ]
-}
-```
-
-**Generate TypeScript types:** Run `npx wrangler types` to auto-generate `worker-configuration.d.ts` from your wrangler.jsonc.
-
-**Multiple configs:**
-```jsonc
-{
- "hyperdrive": [
- {"binding": "HYPERDRIVE_CACHED", "id": ""},
- {"binding": "HYPERDRIVE_NO_CACHE", "id": ""}
- ]
-}
-```
-
-## Management
-
-```bash
-npx wrangler hyperdrive list
-npx wrangler hyperdrive get
-npx wrangler hyperdrive update --max-age=180
-npx wrangler hyperdrive delete
-```
-
-## Config Options
-
-Hyperdrive create/update CLI flags:
-
-| Option | Default | Notes |
-|--------|---------|-------|
-| `--caching-disabled` | `false` | Disable caching |
-| `--max-age` | `60` | Cache TTL (max 3600s) |
-| `--swr` | `15` | Stale-while-revalidate |
-| `--origin-connection-limit` | 20/100 | Free/paid |
-| `--access-client-id` | - | Tunnel auth |
-| `--access-client-secret` | - | Tunnel auth |
-| `--sslmode` | `require` | PostgreSQL only |
-
-## Smart Placement Integration
-
-For Workers making **multiple queries** per request, enable Smart Placement to execute near your database:
-
-```jsonc
-{
- "compatibility_date": "2025-01-01",
- "compatibility_flags": ["nodejs_compat"],
- "placement": {
- "mode": "smart"
- },
- "hyperdrive": [
- {
- "binding": "HYPERDRIVE",
- "id": ""
- }
- ]
-}
-```
-
-**Benefits:** Multi-query Workers run closer to DB, reducing round-trip latency. See [patterns.md](./patterns.md) for examples.
-
-## Private DB via Tunnel
-
-```
-Worker → Hyperdrive → Access → Tunnel → Private Network → DB
-```
-
-**Setup:**
-```bash
-# 1. Create tunnel
-cloudflared tunnel create my-db-tunnel
-
-# 2. Configure hostname in Zero Trust dashboard
-# Domain: db-tunnel.example.com
-# Service: TCP -> localhost:5432
-
-# 3. Create service token (Zero Trust > Service Auth)
-# Save Client ID/Secret
-
-# 4. Create Access app (db-tunnel.example.com)
-# Policy: Service Auth token from step 3
-
-# 5. Create Hyperdrive
-npx wrangler hyperdrive create my-private-db \
- --host=db-tunnel.example.com \
- --user=dbuser --password=dbpass --database=prod \
- --access-client-id= --access-client-secret=
-```
-
-**⚠️ Don't specify `--port` with Tunnel** - port configured in tunnel service settings.
-
-## Local Dev
-
-**Option 1: Local (RECOMMENDED):**
-```bash
-# Env var (takes precedence)
-export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://user:pass@localhost:5432/dev"
-npx wrangler dev
-
-# wrangler.jsonc
-{"hyperdrive": [{"binding": "HYPERDRIVE", "localConnectionString": "postgres://..."}]}
-```
-
-**Remote DB locally:**
-```bash
-# PostgreSQL
-export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://user:pass@remote:5432/db?sslmode=require"
-
-# MySQL
-export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="mysql://user:pass@remote:3306/db?sslMode=REQUIRED"
-```
-
-**Option 2: Remote execution:**
-```bash
-npx wrangler dev --remote # Uses deployed config, affects production
-```
-
-See [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md).
diff --git a/skills/cloudflare/references/hyperdrive/gotchas.md b/skills/cloudflare/references/hyperdrive/gotchas.md
deleted file mode 100644
index efa2ead..0000000
--- a/skills/cloudflare/references/hyperdrive/gotchas.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# Gotchas
-
-See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md).
-
-## Common Errors
-
-### "Too many open connections" / "Connection limit exceeded"
-
-**Cause:** Workers have a hard limit of **6 concurrent connections per invocation**
-**Solution:** Set `max: 5` in driver config, reuse connections, ensure proper cleanup with `client.end()` or `ctx.waitUntil(conn.end())`
-
-### "Failed to acquire a connection (Pool exhausted)"
-
-**Cause:** All connections in pool are in use, often due to long-running transactions
-**Solution:** Reduce transaction duration, avoid queries >60s, don't hold connections during external calls, or upgrade to paid plan for more connections
-
-### "connection_refused"
-
-**Cause:** Database refusing connections due to firewall, connection limits, or service down
-**Solution:** Check firewall allows Cloudflare IPs, verify DB listening on port, confirm service running, and validate credentials
-
-### "Query timeout (deadline exceeded)"
-
-**Cause:** Query execution exceeding 60s timeout limit
-**Solution:** Optimize with indexes, reduce dataset with LIMIT, break into smaller queries, or use async processing
-
-### "password authentication failed"
-
-**Cause:** Invalid credentials in Hyperdrive configuration
-**Solution:** Check username and password in Hyperdrive config match database credentials
-
-### "SSL/TLS connection error"
-
-**Cause:** SSL/TLS configuration mismatch between Hyperdrive and database
-**Solution:** Add `sslmode=require` (Postgres) or `sslMode=REQUIRED` (MySQL), upload CA cert if self-signed, verify DB has SSL enabled, and check cert expiry
-
-### "Queries not being cached"
-
-**Cause:** Query is mutating (INSERT/UPDATE/DELETE), contains volatile functions (NOW(), RANDOM()), or caching disabled
-**Solution:** Verify query is non-mutating SELECT, avoid volatile functions, confirm caching enabled, use `wrangler dev --remote` to test, and set `prepare=true` for postgres.js
-
-### "Slow multi-query Workers despite Hyperdrive"
-
-**Cause:** Worker executing at edge, each query round-trips to DB region
-**Solution:** Enable Smart Placement (`"placement": {"mode": "smart"}` in wrangler.jsonc) to execute Worker near DB. See [patterns.md](./patterns.md) Multi-Query pattern.
-
-### "Local database connection failed"
-
-**Cause:** `localConnectionString` incorrect or database not running
-**Solution:** Verify `localConnectionString` correct, check DB running, confirm env var name matches binding, and test with psql/mysql client
-
-### "Environment variable not working"
-
-**Cause:** Environment variable format incorrect or not exported
-**Solution:** Use format `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_`, ensure binding matches wrangler.jsonc, export variable in shell, and restart wrangler dev
-
-## Limits
-
-| Limit | Free | Paid | Notes |
-|-------|------|------|-------|
-| Max configs | 10 | 25 | Hyperdrive configurations per account |
-| Worker connections | 6 | 6 | Max concurrent connections per Worker invocation |
-| Username/DB name | 63 bytes | 63 bytes | Maximum length |
-| Connection timeout | 15s | 15s | Time to establish connection |
-| Idle timeout | 10 min | 10 min | Connection idle timeout |
-| Max origin connections | ~20 | ~100 | Connections to origin database |
-| Query duration max | 60s | 60s | Queries >60s terminated |
-| Cached response max | 50 MB | 50 MB | Responses >50MB returned but not cached |
-
-## Resources
-
-- [Docs](https://developers.cloudflare.com/hyperdrive/)
-- [Getting Started](https://developers.cloudflare.com/hyperdrive/get-started/)
-- [Wrangler Reference](https://developers.cloudflare.com/hyperdrive/reference/wrangler-commands/)
-- [Supported DBs](https://developers.cloudflare.com/hyperdrive/reference/supported-databases-and-features/)
-- [Discord #hyperdrive](https://discord.cloudflare.com)
-- [Limit Increase Form](https://forms.gle/ukpeZVLWLnKeixDu7)
diff --git a/skills/cloudflare/references/hyperdrive/patterns.md b/skills/cloudflare/references/hyperdrive/patterns.md
deleted file mode 100644
index bd794b9..0000000
--- a/skills/cloudflare/references/hyperdrive/patterns.md
+++ /dev/null
@@ -1,190 +0,0 @@
-# Patterns
-
-See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md).
-
-## High-Traffic Read-Heavy
-
-```typescript
-const sql = postgres(env.HYPERDRIVE.connectionString, {max: 5, prepare: true});
-
-// Cacheable: popular content
-const posts = await sql`SELECT * FROM posts WHERE published = true ORDER BY views DESC LIMIT 20`;
-
-// Cacheable: user profiles
-const [user] = await sql`SELECT id, username, bio FROM users WHERE id = ${userId}`;
-```
-
-**Benefits:** Trending/profiles cached (60s), connection pooling handles spikes.
-
-## Mixed Read/Write
-
-```typescript
-interface Env {
- HYPERDRIVE_CACHED: Hyperdrive; // max_age=120
- HYPERDRIVE_REALTIME: Hyperdrive; // caching disabled
-}
-
-// Reads: cached
-if (req.method === "GET") {
- const sql = postgres(env.HYPERDRIVE_CACHED.connectionString, {prepare: true});
- const products = await sql`SELECT * FROM products WHERE category = ${cat}`;
-}
-
-// Writes: no cache (immediate consistency)
-if (req.method === "POST") {
- const sql = postgres(env.HYPERDRIVE_REALTIME.connectionString, {prepare: true});
- await sql`INSERT INTO orders ${sql(data)}`;
-}
-```
-
-## Analytics Dashboard
-
-```typescript
-const client = new Client({connectionString: env.HYPERDRIVE.connectionString});
-await client.connect();
-
-// Aggregate queries cached (use fixed timestamps for caching)
-const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
-const dailyStats = await client.query(`
- SELECT DATE(created_at) as date, COUNT(*) as orders, SUM(amount) as revenue
- FROM orders WHERE created_at >= $1
- GROUP BY DATE(created_at) ORDER BY date DESC
-`, [thirtyDaysAgo]);
-
-const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
-const topProducts = await client.query(`
- SELECT p.name, COUNT(oi.id) as count, SUM(oi.quantity * oi.price) as revenue
- FROM order_items oi JOIN products p ON oi.product_id = p.id
- WHERE oi.created_at >= $1
- GROUP BY p.id, p.name ORDER BY revenue DESC LIMIT 10
-`, [sevenDaysAgo]);
-```
-
-**Benefits:** Expensive aggregations cached (avoid NOW() for cacheability), dashboard instant, reduced DB load.
-
-## Multi-Tenant
-
-```typescript
-const tenantId = req.headers.get("X-Tenant-ID");
-const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true});
-
-// Tenant-scoped queries cached separately
-const docs = await sql`
- SELECT * FROM documents
- WHERE tenant_id = ${tenantId} AND deleted_at IS NULL
- ORDER BY updated_at DESC LIMIT 50
-`;
-```
-
-**Benefits:** Per-tenant caching, shared connection pool, protects DB from multi-tenant load.
-
-## Geographically Distributed
-
-```typescript
-// Worker runs at edge nearest user
-// Connection setup at edge (fast), pooling near DB (efficient)
-const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true});
-const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`;
-
-return Response.json({
- user,
- serverRegion: req.cf?.colo, // Edge location
-});
-```
-
-**Benefits:** Edge setup + DB pooling = global → single-region DB without replication.
-
-## Multi-Query + Smart Placement
-
-For Workers making **multiple queries** per request, enable Smart Placement to execute near DB:
-
-```jsonc
-// wrangler.jsonc
-{
- "placement": {"mode": "smart"},
- "hyperdrive": [{"binding": "HYPERDRIVE", "id": ""}]
-}
-```
-
-```typescript
-const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: true});
-
-// Multiple queries benefit from Smart Placement
-const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`;
-const orders = await sql`SELECT * FROM orders WHERE user_id = ${userId} ORDER BY created_at DESC LIMIT 10`;
-const stats = await sql`SELECT COUNT(*) as total, SUM(amount) as spent FROM orders WHERE user_id = ${userId}`;
-
-return Response.json({user, orders, stats});
-```
-
-**Benefits:** Worker executes near DB → reduces latency for each query. Without Smart Placement, each query round-trips from edge.
-
-## Connection Pooling
-
-Operates in **transaction mode**: connection acquired per transaction, `RESET` on return.
-
-**SET statements:**
-```typescript
-// ✅ Within transaction
-await client.query("BEGIN");
-await client.query("SET work_mem = '256MB'");
-await client.query("SELECT * FROM large_table"); // Uses SET
-await client.query("COMMIT"); // RESET after
-
-// ✅ Single statement
-await client.query("SET work_mem = '256MB'; SELECT * FROM large_table");
-
-// ❌ Across queries (may get different connection)
-await client.query("SET work_mem = '256MB'");
-await client.query("SELECT * FROM large_table"); // SET not applied
-```
-
-**Best practices:**
-```typescript
-// ❌ Long transactions block pooling
-await client.query("BEGIN");
-await processThousands(); // Connection held entire time
-await client.query("COMMIT");
-
-// ✅ Short transactions
-await client.query("BEGIN");
-await client.query("UPDATE users SET status = $1 WHERE id = $2", [status, id]);
-await client.query("COMMIT");
-
-// ✅ SET LOCAL within transaction
-await client.query("BEGIN");
-await client.query("SET LOCAL work_mem = '256MB'");
-await client.query("SELECT * FROM large_table");
-await client.query("COMMIT");
-```
-
-## Performance Tips
-
-**Enable prepared statements (required for caching):**
-```typescript
-const sql = postgres(connectionString, {prepare: true}); // Default, enables caching
-```
-
-**Optimize connection settings:**
-```typescript
-const sql = postgres(connectionString, {
- max: 5, // Stay under Workers' 6 connection limit
- fetch_types: false, // Reduce latency if not using arrays
- idle_timeout: 60, // Match Worker lifetime
-});
-```
-
-**Write cache-friendly queries:**
-```typescript
-// ✅ Cacheable (deterministic)
-await sql`SELECT * FROM products WHERE category = 'electronics' LIMIT 10`;
-
-// ❌ Not cacheable (volatile NOW())
-await sql`SELECT * FROM logs WHERE created_at > NOW()`;
-
-// ✅ Cacheable (parameterized timestamp)
-const ts = Date.now();
-await sql`SELECT * FROM logs WHERE created_at > ${ts}`;
-```
-
-See [gotchas.md](./gotchas.md) for limits, troubleshooting.
diff --git a/skills/cloudflare/references/images/README.md b/skills/cloudflare/references/images/README.md
deleted file mode 100644
index f1dd644..0000000
--- a/skills/cloudflare/references/images/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Cloudflare Images Skill Reference
-
-**Cloudflare Images** is an end-to-end image management solution providing storage, transformation, optimization, and delivery at scale via Cloudflare's global network.
-
-## Quick Decision Tree
-
-**Need to:**
-- **Transform in Worker?** → [api.md](api.md#workers-binding-api-2026-primary-method) (Workers Binding API)
-- **Upload from Worker?** → [api.md](api.md#upload-from-worker) (REST API)
-- **Upload from client?** → [patterns.md](patterns.md#upload-from-client-direct-creator-upload) (Direct Creator Upload)
-- **Set up variants?** → [configuration.md](configuration.md#variants-configuration)
-- **Serve responsive images?** → [patterns.md](patterns.md#responsive-images)
-- **Add watermarks?** → [patterns.md](patterns.md#watermarking)
-- **Fix errors?** → [gotchas.md](gotchas.md#common-errors)
-
-## Reading Order
-
-**For building image upload/transform feature:**
-1. [configuration.md](configuration.md) - Setup Workers binding
-2. [api.md](api.md#workers-binding-api-2026-primary-method) - Learn transform API
-3. [patterns.md](patterns.md#upload-from-client-direct-creator-upload) - Direct upload pattern
-4. [gotchas.md](gotchas.md) - Check limits and errors
-
-**For URL-based transforms:**
-1. [configuration.md](configuration.md#variants-configuration) - Create variants
-2. [api.md](api.md#url-transform-api) - URL syntax
-3. [patterns.md](patterns.md#responsive-images) - Responsive patterns
-
-**For troubleshooting:**
-1. [gotchas.md](gotchas.md#common-errors) - Error messages
-2. [gotchas.md](gotchas.md#limits) - Size/format limits
-
-## Core Methods
-
-| Method | Use Case | Location |
-|--------|----------|----------|
-| `env.IMAGES.input().transform()` | Transform in Worker | [api.md:11](api.md) |
-| REST API `/images/v1` | Upload images | [api.md:57](api.md) |
-| Direct Creator Upload | Client-side upload | [api.md:127](api.md) |
-| URL transforms | Static image delivery | [api.md:112](api.md) |
-
-## In This Reference
-
-- **[api.md](api.md)** - Complete API: Workers binding, REST endpoints, URL transforms
-- **[configuration.md](configuration.md)** - Setup: wrangler.toml, variants, auth, signed URLs
-- **[patterns.md](patterns.md)** - Patterns: responsive images, watermarks, format negotiation, caching
-- **[gotchas.md](gotchas.md)** - Troubleshooting: limits, errors, best practices
-
-## Key Features
-
-- **Automatic Optimization** - AVIF/WebP format negotiation
-- **On-the-fly Transforms** - Resize, crop, blur, sharpen via URL or API
-- **Workers Binding** - Transform images in Workers (2026 primary method)
-- **Direct Upload** - Secure client-side uploads without backend proxy
-- **Global Delivery** - Cached at 300+ Cloudflare data centers
-- **Watermarking** - Overlay images programmatically
-
-## See Also
-
-- [Official Docs](https://developers.cloudflare.com/images/)
-- [Workers Examples](https://developers.cloudflare.com/images/tutorials/)
diff --git a/skills/cloudflare/references/images/api.md b/skills/cloudflare/references/images/api.md
deleted file mode 100644
index c172e22..0000000
--- a/skills/cloudflare/references/images/api.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# API Reference
-
-## Workers Binding API
-
-```toml
-# wrangler.toml
-[images]
-binding = "IMAGES"
-```
-
-### Transform Images
-
-```typescript
-const imageResponse = await env.IMAGES
- .input(fileBuffer)
- .transform({ width: 800, height: 600, fit: "cover", quality: 85, format: "avif" })
- .output();
-return imageResponse.response();
-```
-
-### Transform Options
-
-```typescript
-interface TransformOptions {
- width?: number; height?: number;
- fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
- quality?: number; // 1-100
- format?: "avif" | "webp" | "jpeg" | "png";
- dpr?: number; // 1-3
- gravity?: "auto" | "left" | "right" | "top" | "bottom" | "face" | string;
- sharpen?: number; // 0-10
- blur?: number; // 1-250
- rotate?: 90 | 180 | 270;
- background?: string; // CSS color for pad
- metadata?: "none" | "copyright" | "keep";
- brightness?: number; contrast?: number; gamma?: number; // 0-2
-}
-```
-
-### Draw/Watermark
-
-```typescript
-await env.IMAGES.input(baseImage)
- .draw(env.IMAGES.input(watermark).transform({ width: 100 }), { top: 10, left: 10, opacity: 0.8 })
- .output();
-```
-
-## REST API
-
-### Upload Image
-
-```bash
-curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
- -H "Authorization: Bearer {token}" -F file=@image.jpg -F metadata='{"key":"value"}'
-```
-
-### Other Operations
-
-```bash
-GET /accounts/{account_id}/images/v1/{image_id} # Get details
-DELETE /accounts/{account_id}/images/v1/{image_id} # Delete
-GET /accounts/{account_id}/images/v1?page=1 # List
-```
-
-## URL Transform API
-
-```
-https://imagedelivery.net/{hash}/{id}/width=800,height=600,fit=cover,format=avif
-```
-
-**Params:** `w=`, `h=`, `fit=`, `q=`, `f=`, `dpr=`, `gravity=`, `sharpen=`, `blur=`, `rotate=`, `background=`, `metadata=`
-
-## Direct Creator Upload
-
-```typescript
-// 1. Get upload URL (backend)
-const { result } = await fetch(
- `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v2/direct_upload`,
- { method: 'POST', headers: { 'Authorization': `Bearer ${token}` },
- body: JSON.stringify({ requireSignedURLs: false }) }
-).then(r => r.json());
-
-// 2. Client uploads to result.uploadURL
-const formData = new FormData();
-formData.append('file', file);
-await fetch(result.uploadURL, { method: 'POST', body: formData });
-```
-
-## Error Codes
-
-| Code | Message | Solution |
-|------|---------|----------|
-| 5400 | Invalid format | Use JPEG, PNG, GIF, WebP |
-| 5401 | Too large | Max 100MB |
-| 5403 | Invalid transform | Check params |
-| 9413 | Rate limit | Implement backoff |
diff --git a/skills/cloudflare/references/images/configuration.md b/skills/cloudflare/references/images/configuration.md
deleted file mode 100644
index 9fa2deb..0000000
--- a/skills/cloudflare/references/images/configuration.md
+++ /dev/null
@@ -1,211 +0,0 @@
-# Configuration
-
-## Wrangler Integration
-
-### Workers Binding Setup
-
-Add to `wrangler.toml`:
-
-```toml
-name = "my-image-worker"
-main = "src/index.ts"
-compatibility_date = "2024-01-01"
-
-[images]
-binding = "IMAGES"
-```
-
-Access in Worker:
-
-```typescript
-interface Env {
- IMAGES: ImageBinding;
-}
-
-export default {
- async fetch(request: Request, env: Env): Promise {
- return await env.IMAGES
- .input(imageBuffer)
- .transform({ width: 800 })
- .output()
- .response();
- }
-};
-```
-
-### Upload via Script
-
-Wrangler doesn't have built-in Images commands, use REST API:
-
-```typescript
-// scripts/upload-image.ts
-import fs from 'fs';
-import FormData from 'form-data';
-
-async function uploadImage(filePath: string) {
- const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!;
- const apiToken = process.env.CLOUDFLARE_API_TOKEN!;
-
- const formData = new FormData();
- formData.append('file', fs.createReadStream(filePath));
-
- const response = await fetch(
- `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
- {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiToken}`,
- },
- body: formData,
- }
- );
-
- const result = await response.json();
- console.log('Uploaded:', result);
-}
-
-uploadImage('./photo.jpg');
-```
-
-### Environment Variables
-
-Store account hash for URL construction:
-
-```toml
-[vars]
-IMAGES_ACCOUNT_HASH = "your-account-hash"
-ACCOUNT_ID = "your-account-id"
-```
-
-Access in Worker:
-
-```typescript
-const imageUrl = `https://imagedelivery.net/${env.IMAGES_ACCOUNT_HASH}/${imageId}/public`;
-```
-
-## Variants Configuration
-
-Variants are named presets for transformations.
-
-### Create Variant (Dashboard)
-
-1. Navigate to Images → Variants
-2. Click "Create Variant"
-3. Set name (e.g., `thumbnail`)
-4. Configure: `width=200,height=200,fit=cover`
-
-### Create Variant (API)
-
-```bash
-curl -X POST \
- https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1/variants \
- -H "Authorization: Bearer {api_token}" \
- -H "Content-Type: application/json" \
- -d '{
- "id": "thumbnail",
- "options": {
- "width": 200,
- "height": 200,
- "fit": "cover"
- },
- "neverRequireSignedURLs": true
- }'
-```
-
-### Use Variant
-
-```
-https://imagedelivery.net/{account_hash}/{image_id}/thumbnail
-```
-
-### Common Variant Presets
-
-```json
-{
- "thumbnail": {
- "width": 200,
- "height": 200,
- "fit": "cover"
- },
- "avatar": {
- "width": 128,
- "height": 128,
- "fit": "cover",
- "gravity": "face"
- },
- "hero": {
- "width": 1920,
- "height": 1080,
- "fit": "cover",
- "quality": 90
- },
- "mobile": {
- "width": 640,
- "fit": "scale-down",
- "quality": 80,
- "format": "avif"
- }
-}
-```
-
-## Authentication
-
-### API Token (Recommended)
-
-Generate at: Dashboard → My Profile → API Tokens
-
-Required permissions:
-- Account → Cloudflare Images → Edit
-
-```bash
-curl -H "Authorization: Bearer {api_token}" \
- https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1
-```
-
-### API Key (Legacy)
-
-```bash
-curl -H "X-Auth-Email: {email}" \
- -H "X-Auth-Key: {api_key}" \
- https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1
-```
-
-## Signed URLs
-
-For private images, enable signed URLs:
-
-```bash
-# Upload with signed URLs required
-curl -X POST \
- https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
- -H "Authorization: Bearer {api_token}" \
- -F file=@private.jpg \
- -F requireSignedURLs=true
-```
-
-Generate signed URL:
-
-```typescript
-import { createHmac } from 'crypto';
-
-function signUrl(imageId: string, variant: string, expiry: number, key: string): string {
- const path = `/${imageId}/${variant}`;
- const toSign = `${path}${expiry}`;
- const signature = createHmac('sha256', key)
- .update(toSign)
- .digest('hex');
-
- return `https://imagedelivery.net/{hash}${path}?exp=${expiry}&sig=${signature}`;
-}
-
-// Sign URL valid for 1 hour
-const signedUrl = signUrl('image-id', 'public', Date.now() + 3600, env.SIGNING_KEY);
-```
-
-## Local Development
-
-```bash
-npx wrangler dev --remote
-```
-
-Must use `--remote` for Images binding access.
diff --git a/skills/cloudflare/references/images/gotchas.md b/skills/cloudflare/references/images/gotchas.md
deleted file mode 100644
index 6f52455..0000000
--- a/skills/cloudflare/references/images/gotchas.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Gotchas & Best Practices
-
-## Fit Modes
-
-| Mode | Best For | Behavior |
-|------|----------|----------|
-| `cover` | Hero images, thumbnails | Fills space, crops excess |
-| `contain` | Product images, artwork | Preserves full image, may add padding |
-| `scale-down` | User uploads | Never enlarges |
-| `crop` | Precise crops | Uses gravity |
-| `pad` | Fixed aspect ratio | Adds background |
-
-## Format Selection
-
-```typescript
-format: 'auto' // Recommended - negotiates best format
-```
-
-**Support:** AVIF (Chrome 85+, Firefox 93+, Safari 16.4+), WebP (Chrome 23+, Firefox 65+, Safari 14+)
-
-## Quality Settings
-
-| Use Case | Quality |
-|----------|---------|
-| Thumbnails | 75-80 |
-| Standard | 85 (default) |
-| High-quality | 90-95 |
-
-## Common Errors
-
-### 5403: "Image transformation failed"
-- Verify `width`/`height` ≤ 12000
-- Check `quality` 1-100, `dpr` 1-3
-- Don't combine incompatible options
-
-### 9413: "Rate limit exceeded"
-Implement caching and exponential backoff:
-```typescript
-for (let i = 0; i < 3; i++) {
- try { return await env.IMAGES.input(buffer).transform({...}).output(); }
- catch { await new Promise(r => setTimeout(r, 2 ** i * 1000)); }
-}
-```
-
-### 5401: "Image too large"
-Pre-process images before upload (max 100MB, 12000×12000px)
-
-### 5400: "Invalid image format"
-Supported: JPEG, PNG, GIF, WebP, AVIF, SVG
-
-### 401/403: "Unauthorized"
-Verify API token has `Cloudflare Images → Edit` permission
-
-## Limits
-
-| Resource | Limit |
-|----------|-------|
-| Max input size | 100MB |
-| Max dimensions | 12000×12000px |
-| Quality range | 1-100 |
-| DPR range | 1-3 |
-| API rate limit | ~1200 req/min |
-
-## AVIF Gotchas
-
-- **Slower encoding**: First request may have higher latency
-- **Browser detection**:
-```typescript
-const format = /image\/avif/.test(request.headers.get('Accept') || '') ? 'avif' : 'webp';
-```
-
-## Anti-Patterns
-
-```typescript
-// ❌ No caching - transforms every request
-return env.IMAGES.input(buffer).transform({...}).output().response();
-
-// ❌ cover without both dimensions
-transform({ width: 800, fit: 'cover' })
-
-// ✅ Always set both for cover
-transform({ width: 800, height: 600, fit: 'cover' })
-
-// ❌ Exposes API token to client
-// ✅ Use Direct Creator Upload (patterns.md)
-```
-
-## Debugging
-
-```typescript
-// Check response headers
-console.log('Content-Type:', response.headers.get('Content-Type'));
-
-// Test with curl
-// curl -I "https://imagedelivery.net/{hash}/{id}/width=800,format=avif"
-
-// Monitor logs
-// npx wrangler tail
-```
diff --git a/skills/cloudflare/references/images/patterns.md b/skills/cloudflare/references/images/patterns.md
deleted file mode 100644
index c07bf3c..0000000
--- a/skills/cloudflare/references/images/patterns.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# Common Patterns
-
-## URL Transform Options
-
-```
-width= height= fit=scale-down|contain|cover|crop|pad
-quality=85 format=auto|webp|avif|jpeg|png dpr=2
-gravity=auto|face|left|right|top|bottom sharpen=2 blur=10
-rotate=90|180|270 background=white metadata=none|copyright|keep
-```
-
-## Responsive Images (srcset)
-
-```html
-
-```
-
-## Format Negotiation
-
-```typescript
-async fetch(request: Request, env: Env): Promise {
- const accept = request.headers.get('Accept') || '';
- const format = /image\/avif/.test(accept) ? 'avif' : /image\/webp/.test(accept) ? 'webp' : 'jpeg';
- return env.IMAGES.input(buffer).transform({ format, quality: 85 }).output().response();
-}
-```
-
-## Direct Creator Upload
-
-```typescript
-// Backend: Generate upload URL
-const response = await fetch(
- `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/images/v2/direct_upload`,
- { method: 'POST', headers: { 'Authorization': `Bearer ${env.API_TOKEN}` },
- body: JSON.stringify({ requireSignedURLs: false, metadata: { userId } }) }
-);
-
-// Frontend: Upload to returned uploadURL
-const formData = new FormData();
-formData.append('file', file);
-await fetch(result.uploadURL, { method: 'POST', body: formData });
-// Use: https://imagedelivery.net/{hash}/${result.id}/public
-```
-
-## Transform & Store to R2
-
-```typescript
-async fetch(request: Request, env: Env): Promise {
- const file = (await request.formData()).get('image') as File;
- const transformed = await env.IMAGES
- .input(await file.arrayBuffer())
- .transform({ width: 800, format: 'avif', quality: 80 })
- .output();
- await env.R2.put(`images/${Date.now()}.avif`, transformed.response().body);
- return Response.json({ success: true });
-}
-```
-
-## Watermarking
-
-```typescript
-const watermark = await env.ASSETS.fetch(new URL('/watermark.png', request.url));
-const result = await env.IMAGES
- .input(await image.arrayBuffer())
- .draw(env.IMAGES.input(watermark.body).transform({ width: 100 }), { bottom: 20, right: 20, opacity: 0.7 })
- .transform({ format: 'avif' })
- .output();
-return result.response();
-```
-
-## Device-Based Transforms
-
-```typescript
-const ua = request.headers.get('User-Agent') || '';
-const isMobile = /Mobile|Android|iPhone/i.test(ua);
-return env.IMAGES.input(buffer)
- .transform({ width: isMobile ? 400 : 1200, quality: isMobile ? 75 : 85, format: 'avif' })
- .output().response();
-```
-
-## Caching Strategy
-
-```typescript
-async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
- const cache = caches.default;
- let response = await cache.match(request);
- if (!response) {
- response = await env.IMAGES.input(buffer).transform({ width: 800, format: 'avif' }).output().response();
- response = new Response(response.body, { headers: { ...response.headers, 'Cache-Control': 'public, max-age=86400' } });
- ctx.waitUntil(cache.put(request, response.clone()));
- }
- return response;
-}
-```
-
-## Batch Processing
-
-```typescript
-const results = await Promise.all(images.map(buffer =>
- env.IMAGES.input(buffer).transform({ width: 800, fit: 'cover', format: 'avif' }).output()
-));
-```
-
-## Error Handling
-
-```typescript
-try {
- return (await env.IMAGES.input(buffer).transform({ width: 800 }).output()).response();
-} catch (error) {
- console.error('Transform failed:', error);
- return new Response('Image processing failed', { status: 500 });
-}
-```
diff --git a/skills/cloudflare/references/kv/README.md b/skills/cloudflare/references/kv/README.md
deleted file mode 100644
index 9e43e01..0000000
--- a/skills/cloudflare/references/kv/README.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Cloudflare Workers KV
-
-Globally-distributed, eventually-consistent key-value store optimized for high read volume and low latency.
-
-## Overview
-
-KV provides:
-- Eventual consistency (60s global propagation)
-- Read-optimized performance
-- 25 MiB value limit per key
-- Auto-replication to Cloudflare edge
-- Metadata support (1024 bytes)
-
-**Use cases:** Config storage, user sessions, feature flags, caching, A/B testing
-
-## When to Use KV
-
-| Need | Recommendation |
-|------|----------------|
-| Strong consistency | → [Durable Objects](../durable-objects/) |
-| SQL queries | → [D1](../d1/) |
-| Object storage (files) | → [R2](../r2/) |
-| High read, low write volume | → KV ✅ |
-| Sub-10ms global reads | → KV ✅ |
-
-**Quick comparison:**
-
-| Feature | KV | D1 | Durable Objects |
-|---------|----|----|-----------------|
-| Consistency | Eventual | Strong | Strong |
-| Read latency | <10ms | ~50ms | <1ms |
-| Write limit | 1/s per key | Unlimited | Unlimited |
-| Use case | Config, cache | Relational data | Coordination |
-
-## Quick Start
-
-```bash
-wrangler kv namespace create MY_NAMESPACE
-# Add binding to wrangler.jsonc
-```
-
-```typescript
-// Write
-await env.MY_KV.put("key", "value", { expirationTtl: 300 });
-
-// Read
-const value = await env.MY_KV.get("key");
-const json = await env.MY_KV.get("config", "json");
-```
-
-## Core Operations
-
-| Method | Purpose | Returns |
-|--------|---------|---------|
-| `get(key, type?)` | Single read | `string \| null` |
-| `get(keys, type?)` | Bulk read (≤100) | `Map` |
-| `put(key, value, options?)` | Write | `Promise` |
-| `delete(key)` | Delete | `Promise` |
-| `list(options?)` | List keys | `{ keys, list_complete, cursor? }` |
-| `getWithMetadata(key)` | Get + metadata | `{ value, metadata }` |
-
-## Consistency Model
-
-- **Write visibility:** Immediate in same location, ≤60s globally
-- **Read path:** Eventually consistent
-- **Write rate:** 1 write/second per key (429 on exceed)
-
-## Reading Order
-
-| Task | Files to Read |
-|------|---------------|
-| Quick start | README → configuration.md |
-| Implement feature | README → api.md → patterns.md |
-| Debug issues | gotchas.md → api.md |
-| Batch operations | api.md (bulk section) → patterns.md |
-| Performance tuning | gotchas.md (performance) → patterns.md (caching) |
-
-## In This Reference
-
-- [configuration.md](./configuration.md) - wrangler.jsonc setup, namespace creation, TypeScript types
-- [api.md](./api.md) - KV methods, bulk operations, cacheTtl, content types
-- [patterns.md](./patterns.md) - Caching, sessions, rate limiting, A/B testing
-- [gotchas.md](./gotchas.md) - Eventual consistency, concurrent writes, value limits
-
-## See Also
-
-- [workers](../workers/) - Worker runtime for KV access
-- [d1](../d1/) - Use D1 for strong consistency needs
-- [durable-objects](../durable-objects/) - Strongly consistent alternative
diff --git a/skills/cloudflare/references/kv/api.md b/skills/cloudflare/references/kv/api.md
deleted file mode 100644
index 35063f2..0000000
--- a/skills/cloudflare/references/kv/api.md
+++ /dev/null
@@ -1,160 +0,0 @@
-# KV API Reference
-
-## Read Operations
-
-```typescript
-// Single key (string)
-const value = await env.MY_KV.get("user:123");
-
-// JSON type (auto-parsed)
-const config = await env.MY_KV.get("config", "json");
-
-// ArrayBuffer for binary
-const buffer = await env.MY_KV.get("image", "arrayBuffer");
-
-// Stream for large values
-const stream = await env.MY_KV.get("large-file", "stream");
-
-// With cache TTL (min 60s)
-const value = await env.MY_KV.get("key", { type: "text", cacheTtl: 300 });
-
-// Bulk get (max 100 keys, counts as 1 operation)
-const keys = ["user:1", "user:2", "user:3", "missing:key"];
-const results = await env.MY_KV.get(keys);
-// Returns Map
-
-console.log(results.get("user:1")); // "John" (if exists)
-console.log(results.get("missing:key")); // null
-
-// Process results with null handling
-for (const [key, value] of results) {
- if (value !== null) {
- // Handle found keys
- console.log(`${key}: ${value}`);
- }
-}
-
-// TypeScript with generics (type-safe JSON parsing)
-interface UserProfile { name: string; email: string; }
-const profile = await env.USERS.get("user:123", "json");
-// profile is typed as UserProfile | null
-if (profile) {
- console.log(profile.name); // Type-safe access
-}
-
-// Bulk get with type
-const configs = await env.MY_KV.get(["config:app", "config:feature"], "json");
-// Map
-```
-
-## Write Operations
-
-```typescript
-// Basic put
-await env.MY_KV.put("key", "value");
-await env.MY_KV.put("config", JSON.stringify({ theme: "dark" }));
-
-// With expiration (UNIX timestamp)
-await env.MY_KV.put("session", token, {
- expiration: Math.floor(Date.now() / 1000) + 3600
-});
-
-// With TTL (seconds from now, min 60)
-await env.MY_KV.put("cache", data, { expirationTtl: 300 });
-
-// With metadata (max 1024 bytes)
-await env.MY_KV.put("user:profile", userData, {
- metadata: { version: 2, lastUpdated: Date.now() }
-});
-
-// Combined
-await env.MY_KV.put("temp", value, {
- expirationTtl: 3600,
- metadata: { temporary: true }
-});
-```
-
-## Get with Metadata
-
-```typescript
-// Single key
-const result = await env.MY_KV.getWithMetadata("user:profile");
-// { value: string | null, metadata: any | null }
-
-if (result.value && result.metadata) {
- const { version, lastUpdated } = result.metadata;
-}
-
-// Multiple keys (bulk)
-const keys = ["key1", "key2", "key3"];
-const results = await env.MY_KV.getWithMetadata(keys);
-// Returns Map
-
-for (const [key, result] of results) {
- if (result.value) {
- console.log(`${key}: ${result.value}`);
- console.log(`Metadata: ${JSON.stringify(result.metadata)}`);
- // cacheStatus field indicates cache hit/miss (when available)
- }
-}
-
-// With type
-const result = await env.MY_KV.getWithMetadata("user:123", "json");
-// result: { value: UserData | null, metadata: any | null, cacheStatus?: string }
-```
-
-## Delete Operations
-
-```typescript
-await env.MY_KV.delete("key"); // Always succeeds (even if key missing)
-```
-
-## List Operations
-
-```typescript
-// List all
-const keys = await env.MY_KV.list();
-// { keys: [...], list_complete: boolean, cursor?: string }
-
-// With prefix
-const userKeys = await env.MY_KV.list({ prefix: "user:" });
-
-// Pagination
-let cursor: string | undefined;
-let allKeys = [];
-do {
- const result = await env.MY_KV.list({ cursor, limit: 1000 });
- allKeys.push(...result.keys);
- cursor = result.cursor;
-} while (!result.list_complete);
-```
-
-## Performance Considerations
-
-### Type Selection
-
-| Type | Use Case | Performance |
-|------|----------|-------------|
-| `stream` | Large values (>1MB) | Fastest - no buffering |
-| `arrayBuffer` | Binary data | Fast - single allocation |
-| `text` | String values | Medium |
-| `json` | Objects (parse overhead) | Slowest - parsing cost |
-
-### Parallel Reads
-
-```typescript
-// Efficient parallel reads with Promise.all()
-const [user, settings, cache] = await Promise.all([
- env.USERS.get("user:123", "json"),
- env.SETTINGS.get("config:app", "json"),
- env.CACHE.get("data:latest")
-]);
-```
-
-## Error Handling
-
-- **Missing keys:** Return `null` (not an error)
-- **Rate limit (429):** Retry with exponential backoff (see gotchas.md)
-- **Response too large (413):** Values >25MB fail with 413 error
-
-See [gotchas.md](./gotchas.md) for detailed error patterns and solutions.
diff --git a/skills/cloudflare/references/kv/configuration.md b/skills/cloudflare/references/kv/configuration.md
deleted file mode 100644
index 0aefa5f..0000000
--- a/skills/cloudflare/references/kv/configuration.md
+++ /dev/null
@@ -1,144 +0,0 @@
-# KV Configuration
-
-## Create Namespace
-
-```bash
-wrangler kv namespace create MY_NAMESPACE
-# Output: { binding = "MY_NAMESPACE", id = "abc123..." }
-
-wrangler kv namespace create MY_NAMESPACE --preview # For local dev
-```
-
-## Workers Binding
-
-**wrangler.jsonc:**
-```jsonc
-{
- "kv_namespaces": [
- {
- "binding": "MY_KV",
- "id": "abc123xyz789"
- },
- // Optional: Different namespace for preview/development
- {
- "binding": "MY_KV",
- "preview_id": "preview-abc123"
- }
- ]
-}
-```
-
-## TypeScript Types
-
-**env.d.ts:**
-```typescript
-interface Env {
- MY_KV: KVNamespace;
- SESSIONS: KVNamespace;
- CACHE: KVNamespace;
-}
-```
-
-**worker.ts:**
-```typescript
-export default {
- async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
- // env.MY_KV is now typed as KVNamespace
- const value = await env.MY_KV.get("key");
- return new Response(value || "Not found");
- }
-} satisfies ExportedHandler;
-```
-
-**Type-safe JSON operations:**
-```typescript
-interface UserProfile {
- name: string;
- email: string;
- role: "admin" | "user";
-}
-
-const profile = await env.USERS.get("user:123", "json");
-// profile: UserProfile | null (type-safe!)
-if (profile) {
- console.log(profile.name); // TypeScript knows this is a string
-}
-```
-
-## CLI Operations
-
-```bash
-# Put
-wrangler kv key put --binding=MY_KV "key" "value"
-wrangler kv key put --binding=MY_KV "key" --path=./file.json --ttl=3600
-
-# Get
-wrangler kv key get --binding=MY_KV "key"
-
-# Delete
-wrangler kv key delete --binding=MY_KV "key"
-
-# List
-wrangler kv key list --binding=MY_KV --prefix="user:"
-
-# Bulk operations (max 10,000 keys per file)
-wrangler kv bulk put data.json --binding=MY_KV
-wrangler kv bulk get keys.json --binding=MY_KV
-wrangler kv bulk delete keys.json --binding=MY_KV --force
-```
-
-## Local Development
-
-```bash
-wrangler dev # Local KV (isolated)
-wrangler dev --remote # Remote KV (production)
-
-# Or in wrangler.jsonc:
-# "kv_namespaces": [{ "binding": "MY_KV", "id": "...", "remote": true }]
-```
-
-## REST API
-
-### Single Operations
-
-```typescript
-import Cloudflare from 'cloudflare';
-
-const client = new Cloudflare({
- apiEmail: process.env.CLOUDFLARE_EMAIL,
- apiKey: process.env.CLOUDFLARE_API_KEY
-});
-
-// Single key operations
-await client.kv.namespaces.values.update(namespaceId, 'key', {
- account_id: accountId,
- value: 'value',
- expiration_ttl: 3600
-});
-```
-
-### Bulk Operations
-
-```typescript
-// Bulk update (up to 10,000 keys, max 100MB total)
-await client.kv.namespaces.bulkUpdate(namespaceId, {
- account_id: accountId,
- body: [
- { key: "key1", value: "value1", expiration_ttl: 3600 },
- { key: "key2", value: "value2", metadata: { version: 1 } },
- { key: "key3", value: "value3" }
- ]
-});
-
-// Bulk get (up to 100 keys)
-const results = await client.kv.namespaces.bulkGet(namespaceId, {
- account_id: accountId,
- keys: ["key1", "key2", "key3"]
-});
-
-// Bulk delete (up to 10,000 keys)
-await client.kv.namespaces.bulkDelete(namespaceId, {
- account_id: accountId,
- keys: ["key1", "key2", "key3"]
-});
-```
diff --git a/skills/cloudflare/references/kv/gotchas.md b/skills/cloudflare/references/kv/gotchas.md
deleted file mode 100644
index 92242da..0000000
--- a/skills/cloudflare/references/kv/gotchas.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# KV Gotchas & Troubleshooting
-
-## Common Errors
-
-### "Stale Read After Write"
-
-**Cause:** Eventual consistency means writes may not be immediately visible in other regions
-**Solution:** Don't read immediately after write; return confirmation without reading or use the local value you just wrote. Writes visible immediately in same location, ≤60s globally
-
-```typescript
-// ❌ BAD: Read immediately after write
-await env.KV.put("key", "value");
-const value = await env.KV.get("key"); // May be null in other regions!
-
-// ✅ GOOD: Use the value you just wrote
-const newValue = "value";
-await env.KV.put("key", newValue);
-return new Response(newValue); // Don't re-read
-```
-
-### "429 Rate Limit on Concurrent Writes"
-
-**Cause:** Multiple concurrent writes to same key exceeding 1 write/second limit
-**Solution:** Use sequential writes, unique keys for concurrent operations, or implement retry with exponential backoff
-
-```typescript
-async function putWithRetry(
- kv: KVNamespace,
- key: string,
- value: string,
- maxAttempts = 5
-): Promise {
- let delay = 1000;
- for (let i = 0; i < maxAttempts; i++) {
- try {
- await kv.put(key, value);
- return;
- } catch (err) {
- if (err instanceof Error && err.message.includes("429")) {
- if (i === maxAttempts - 1) throw err;
- await new Promise(r => setTimeout(r, delay));
- delay *= 2; // Exponential backoff
- } else {
- throw err;
- }
- }
- }
-}
-```
-
-### "Inefficient Multiple Gets"
-
-**Cause:** Making multiple individual get() calls instead of bulk operation
-**Solution:** Use bulk get with array of keys: `env.USERS.get(["user:1", "user:2", "user:3"])` to reduce to 1 operation
-
-### "Null Reference Error"
-
-**Cause:** Attempting to use value without checking for null when key doesn't exist
-**Solution:** Always handle null returns - KV returns `null` for missing keys, not undefined
-
-```typescript
-// ❌ BAD: Assumes value exists
-const config = await env.KV.get("config", "json");
-return config.theme; // TypeError if null!
-
-// ✅ GOOD: Null checks
-const config = await env.KV.get("config", "json");
-return config?.theme ?? "default";
-
-// ✅ GOOD: Early return
-const config = await env.KV.get("config", "json");
-if (!config) return new Response("Not found", { status: 404 });
-return new Response(config.theme);
-```
-
-### "Negative Lookup Caching"
-
-**Cause:** Keys that don't exist are cached as "not found" for up to 60s
-**Solution:** Creating a key after checking won't be visible until cache expires
-
-```typescript
-// Check → create pattern has race condition
-const exists = await env.KV.get("key"); // null, cached as "not found"
-if (!exists) {
- await env.KV.put("key", "value");
- // Next get() may still return null for ~60s due to negative cache
-}
-
-// Alternative: Always assume key may not exist, use defaults
-const value = await env.KV.get("key") ?? "default-value";
-```
-
-## Performance Tips
-
-| Scenario | Recommendation | Why |
-|----------|----------------|-----|
-| Large values (>1MB) | Use `stream` type | Avoids buffering entire value in memory |
-| Many small keys | Coalesce into one JSON object | Reduces operations, improves cache hit rate |
-| High write volume | Spread across different keys | Avoid 1 write/second per-key limit |
-| Cold reads | Increase `cacheTtl` parameter | Reduces latency for frequently-read data |
-| Bulk operations | Use array form of get() | Single operation, better performance |
-
-## Cost Examples
-
-**Free tier:**
-- 100K reads/day = 3M/month ✅
-- 1K writes/day = 30K/month ✅
-- 1GB storage ✅
-
-**Example paid workload:**
-- 10M reads/month = $5.00
-- 100K writes/month = $0.50
-- 1GB storage = $0.50
-- **Total: ~$6/month**
-
-## Limits
-
-| Limit | Value | Notes |
-|-------|-------|-------|
-| Key size | 512 bytes | Maximum key length |
-| Value size | 25 MiB | Maximum value; 413 error if exceeded |
-| Metadata size | 1024 bytes | Maximum metadata per key |
-| cacheTtl minimum | 60s | Minimum cache TTL |
-| Write rate per key | 1 write/second | All plans; 429 error if exceeded |
-| Propagation time | ≤60s | Global propagation time |
-| Bulk get max | 100 keys | Maximum keys per bulk operation |
-| Operations per Worker | 1,000 | Per request (bulk counts as 1) |
-| Reads pricing | $0.50 per 1M | Per million reads |
-| Writes pricing | $5.00 per 1M | Per million writes |
-| Deletes pricing | $5.00 per 1M | Per million deletes |
-| Storage pricing | $0.50 per GB-month | Per GB per month |
diff --git a/skills/cloudflare/references/kv/patterns.md b/skills/cloudflare/references/kv/patterns.md
deleted file mode 100644
index 8386074..0000000
--- a/skills/cloudflare/references/kv/patterns.md
+++ /dev/null
@@ -1,196 +0,0 @@
-# KV Patterns & Best Practices
-
-## Multi-Tier Caching
-
-```typescript
-// Memory → KV → Origin (3-tier cache)
-const memoryCache = new Map();
-
-async function getCached(env: Env, key: string): Promise {
- const now = Date.now();
-
- // L1: Memory cache (fastest)
- const cached = memoryCache.get(key);
- if (cached && cached.expires > now) {
- return cached.data;
- }
-
- // L2: KV cache (fast)
- const kvValue = await env.CACHE.get(key, "json");
- if (kvValue) {
- memoryCache.set(key, { data: kvValue, expires: now + 60000 }); // 1min in memory
- return kvValue;
- }
-
- // L3: Origin (slow)
- const origin = await fetch(`https://api.example.com/${key}`).then(r => r.json());
-
- // Backfill caches
- await env.CACHE.put(key, JSON.stringify(origin), { expirationTtl: 300 }); // 5min in KV
- memoryCache.set(key, { data: origin, expires: now + 60000 });
-
- return origin;
-}
-```
-
-## API Response Caching
-
-```typescript
-async function getCachedData(env: Env, key: string, fetcher: () => Promise): Promise {
- const cached = await env.MY_KV.get(key, "json");
- if (cached) return cached;
-
- const data = await fetcher();
- await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: 300 });
- return data;
-}
-
-const apiData = await getCachedData(
- env,
- "cache:users",
- () => fetch("https://api.example.com/users").then(r => r.json())
-);
-```
-
-## Session Management
-
-```typescript
-interface Session { userId: string; expiresAt: number; }
-
-async function createSession(env: Env, userId: string): Promise {
- const sessionId = crypto.randomUUID();
- const expiresAt = Date.now() + (24 * 60 * 60 * 1000);
-
- await env.SESSIONS.put(
- `session:${sessionId}`,
- JSON.stringify({ userId, expiresAt }),
- { expirationTtl: 86400, metadata: { createdAt: Date.now() } }
- );
-
- return sessionId;
-}
-
-async function getSession(env: Env, sessionId: string): Promise