{
- 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/.agents/skills/cloudflare-deploy/references/email-routing/configuration.md b/.agents/skills/cloudflare-deploy/references/email-routing/configuration.md
deleted file mode 100644
index 3f9613e..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/email-routing/gotchas.md b/.agents/skills/cloudflare-deploy/references/email-routing/gotchas.md
deleted file mode 100644
index 20ea419..0000000
--- a/.agents/skills/cloudflare-deploy/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 | 50ms |
-| 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/.agents/skills/cloudflare-deploy/references/email-routing/patterns.md b/.agents/skills/cloudflare-deploy/references/email-routing/patterns.md
deleted file mode 100644
index 2163677..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/email-workers/README.md b/.agents/skills/cloudflare-deploy/references/email-workers/README.md
deleted file mode 100644
index 5a3e304..0000000
--- a/.agents/skills/cloudflare-deploy/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) | 50ms |
-
-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/.agents/skills/cloudflare-deploy/references/email-workers/api.md b/.agents/skills/cloudflare-deploy/references/email-workers/api.md
deleted file mode 100644
index 74da66c..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/email-workers/configuration.md b/.agents/skills/cloudflare-deploy/references/email-workers/configuration.md
deleted file mode 100644
index 7928d04..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/email-workers/gotchas.md b/.agents/skills/cloudflare-deploy/references/email-workers/gotchas.md
deleted file mode 100644
index 3700a50..0000000
--- a/.agents/skills/cloudflare-deploy/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 / 50ms |
-| 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/.agents/skills/cloudflare-deploy/references/email-workers/patterns.md b/.agents/skills/cloudflare-deploy/references/email-workers/patterns.md
deleted file mode 100644
index f1e65f5..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/hyperdrive/README.md b/.agents/skills/cloudflare-deploy/references/hyperdrive/README.md
deleted file mode 100644
index 6626776..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/hyperdrive/api.md b/.agents/skills/cloudflare-deploy/references/hyperdrive/api.md
deleted file mode 100644
index 0e587b9..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/hyperdrive/configuration.md b/.agents/skills/cloudflare-deploy/references/hyperdrive/configuration.md
deleted file mode 100644
index 6d429a9..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/hyperdrive/gotchas.md b/.agents/skills/cloudflare-deploy/references/hyperdrive/gotchas.md
deleted file mode 100644
index efa2ead..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/hyperdrive/patterns.md b/.agents/skills/cloudflare-deploy/references/hyperdrive/patterns.md
deleted file mode 100644
index bd794b9..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/images/README.md b/.agents/skills/cloudflare-deploy/references/images/README.md
deleted file mode 100644
index f1dd644..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/images/api.md b/.agents/skills/cloudflare-deploy/references/images/api.md
deleted file mode 100644
index c172e22..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/images/configuration.md b/.agents/skills/cloudflare-deploy/references/images/configuration.md
deleted file mode 100644
index 9fa2deb..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/images/gotchas.md b/.agents/skills/cloudflare-deploy/references/images/gotchas.md
deleted file mode 100644
index 6f52455..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/images/patterns.md b/.agents/skills/cloudflare-deploy/references/images/patterns.md
deleted file mode 100644
index c07bf3c..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/kv/README.md b/.agents/skills/cloudflare-deploy/references/kv/README.md
deleted file mode 100644
index 9e43e01..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/kv/api.md b/.agents/skills/cloudflare-deploy/references/kv/api.md
deleted file mode 100644
index 35063f2..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/kv/configuration.md b/.agents/skills/cloudflare-deploy/references/kv/configuration.md
deleted file mode 100644
index 0aefa5f..0000000
--- a/.agents/skills/cloudflare-deploy/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/.agents/skills/cloudflare-deploy/references/kv/gotchas.md b/.agents/skills/cloudflare-deploy/references/kv/gotchas.md
deleted file mode 100644
index 5ad3213..0000000
--- a/.agents/skills/cloudflare-deploy/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 10M | 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/.agents/skills/cloudflare-deploy/references/kv/patterns.md b/.agents/skills/cloudflare-deploy/references/kv/patterns.md
deleted file mode 100644
index 8386074..0000000
--- a/.agents/skills/cloudflare-deploy/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 {
- const data = await env.SESSIONS.get(`session:${sessionId}`, "json");
- if (!data || data.expiresAt < Date.now()) return null;
- return data;
-}
-```
-
-## Coalesce Cold Keys
-
-```typescript
-// ❌ BAD: Many individual keys
-await env.KV.put("user:123:name", "John");
-await env.KV.put("user:123:email", "john@example.com");
-
-// ✅ GOOD: Single coalesced object
-await env.USERS.put("user:123:profile", JSON.stringify({
- name: "John",
- email: "john@example.com",
- role: "admin"
-}));
-
-// Benefits: Hot key cache, single read, reduced operations
-// Trade-off: Harder to update individual fields
-```
-
-## Prefix-Based Namespacing
-
-```typescript
-// Logical partitioning within single namespace
-const PREFIXES = {
- users: "user:",
- sessions: "session:",
- cache: "cache:",
- features: "feature:"
-} as const;
-
-// Write with prefix
-async function setUser(env: Env, id: string, data: any) {
- await env.KV.put(`${PREFIXES.users}${id}`, JSON.stringify(data));
-}
-
-// Read with prefix
-async function getUser(env: Env, id: string) {
- return await env.KV.get(`${PREFIXES.users}${id}`, "json");
-}
-
-// List by prefix
-async function listUserIds(env: Env): Promise {
- const result = await env.KV.list({ prefix: PREFIXES.users });
- return result.keys.map(k => k.name.replace(PREFIXES.users, ""));
-}
-
-// Example hierarchy
-"user:123:profile"
-"user:123:settings"
-"cache:api:users"
-"session:abc-def"
-"feature:flags:beta"
-```
-
-## Metadata Versioning
-
-```typescript
-interface VersionedData {
- version: number;
- data: any;
-}
-
-async function migrateIfNeeded(env: Env, key: string) {
- const result = await env.DATA.getWithMetadata(key, "json");
-
- if (!result.value) return null;
-
- const currentVersion = result.metadata?.version || 1;
- const targetVersion = 2;
-
- if (currentVersion < targetVersion) {
- // Migrate data format
- const migrated = migrate(result.value, currentVersion, targetVersion);
-
- // Store with new version
- await env.DATA.put(key, JSON.stringify(migrated), {
- metadata: { version: targetVersion, migratedAt: Date.now() }
- });
-
- return migrated;
- }
-
- return result.value;
-}
-
-function migrate(data: any, from: number, to: number): any {
- if (from === 1 && to === 2) {
- // V1 → V2: Rename field
- return { ...data, userName: data.name };
- }
- return data;
-}
-```
-
-## Error Boundary Pattern
-
-```typescript
-// Resilient get with fallback
-async function resilientGet(
- env: Env,
- key: string,
- fallback: T
-): Promise {
- try {
- const value = await env.KV.get(key, "json");
- return value ?? fallback;
- } catch (err) {
- console.error(`KV error for ${key}:`, err);
- return fallback;
- }
-}
-
-// Usage
-const config = await resilientGet(env, "config:app", {
- theme: "light",
- maxItems: 10
-});
-```
diff --git a/.agents/skills/cloudflare-deploy/references/miniflare/README.md b/.agents/skills/cloudflare-deploy/references/miniflare/README.md
deleted file mode 100644
index 82baf7c..0000000
--- a/.agents/skills/cloudflare-deploy/references/miniflare/README.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Miniflare
-
-Local simulator for Cloudflare Workers development/testing. Runs Workers in workerd sandbox implementing runtime APIs - no internet required.
-
-## Features
-
-- Full-featured: KV, Durable Objects, R2, D1, WebSockets, Queues
-- Fully-local: test without internet, instant reload
-- TypeScript-native: detailed logging, source maps
-- Advanced testing: dispatch events without HTTP, simulate Worker connections
-
-## When to Use
-
-**Decision tree for testing Workers:**
-
-```
-Need to test Workers?
-│
-├─ Unit tests for business logic only?
-│ └─ getPlatformProxy (Vitest/Jest) → [patterns.md](./patterns.md#getplatformproxy)
-│ Fast, no HTTP, direct binding access
-│
-├─ Integration tests with full runtime?
-│ ├─ Single Worker?
-│ │ └─ Miniflare API → [Quick Start](#quick-start)
-│ │ Full control, programmatic access
-│ │
-│ ├─ Multiple Workers + service bindings?
-│ │ └─ Miniflare workers array → [configuration.md](./configuration.md#multiple-workers)
-│ │ Shared storage, inter-worker calls
-│ │
-│ └─ Vitest test runner integration?
-│ └─ vitest-pool-workers → [patterns.md](./patterns.md#vitest-pool-workers)
-│ Full Workers env in Vitest
-│
-└─ Local dev server?
- └─ wrangler dev (not Miniflare)
- Hot reload, automatic config
-```
-
-**Use Miniflare for:**
-- Integration tests with full Worker runtime
-- Testing bindings/storage locally
-- Multiple Workers with service bindings
-- Programmatic event dispatch (fetch, queue, scheduled)
-
-**Use getPlatformProxy for:**
-- Fast unit tests of business logic
-- Testing without HTTP overhead
-- Vitest/Jest environments
-
-**Use Wrangler for:**
-- Local development workflow
-- Production deployments
-
-## Setup
-
-```bash
-npm i -D miniflare
-```
-
-Requires ES modules in `package.json`:
-```json
-{"type": "module"}
-```
-
-## Quick Start
-
-```js
-import { Miniflare } from "miniflare";
-
-const mf = new Miniflare({
- modules: true,
- script: `
- export default {
- async fetch(request, env, ctx) {
- return new Response("Hello Miniflare!");
- }
- }
- `,
-});
-
-const res = await mf.dispatchFetch("http://localhost:8787/");
-console.log(await res.text()); // Hello Miniflare!
-await mf.dispose();
-```
-
-## Reading Order
-
-**New to Miniflare?** Start here:
-1. [Quick Start](#quick-start) - Running in 2 minutes
-2. [When to Use](#when-to-use) - Choose your testing approach
-3. [patterns.md](./patterns.md) - Testing patterns (getPlatformProxy, Vitest, node:test)
-4. [configuration.md](./configuration.md) - Configure bindings, storage, multiple workers
-
-**Troubleshooting:**
-- [gotchas.md](./gotchas.md) - Common errors and debugging
-
-**API reference:**
-- [api.md](./api.md) - Complete method reference
-
-## See Also
-- [wrangler](../wrangler/) - CLI tool that embeds Miniflare for `wrangler dev`
-- [workerd](../workerd/) - Runtime that powers Miniflare
-- [workers](../workers/) - Workers runtime API documentation
diff --git a/.agents/skills/cloudflare-deploy/references/miniflare/api.md b/.agents/skills/cloudflare-deploy/references/miniflare/api.md
deleted file mode 100644
index e4df4d7..0000000
--- a/.agents/skills/cloudflare-deploy/references/miniflare/api.md
+++ /dev/null
@@ -1,187 +0,0 @@
-# Programmatic API
-
-## Miniflare Class
-
-```typescript
-class Miniflare {
- constructor(options: MiniflareOptions);
-
- // Lifecycle
- ready: Promise; // Resolves when server ready, returns URL
- dispose(): Promise; // Cleanup resources
- setOptions(options: MiniflareOptions): Promise; // Reload config
-
- // Event dispatching
- dispatchFetch(url: string | URL | Request, init?: RequestInit): Promise;
- getWorker(name?: string): Promise;
-
- // Bindings access
- getBindings>(name?: string): Promise;
- getCf(name?: string): Promise;
- getKVNamespace(name: string): Promise;
- getR2Bucket(name: string): Promise;
- getDurableObjectNamespace(name: string): Promise;
- getDurableObjectStorage(id: DurableObjectId): Promise;
- getD1Database(name: string): Promise;
- getCaches(): Promise;
- getQueueProducer(name: string): Promise;
-
- // Debugging
- getInspectorURL(): Promise; // Chrome DevTools inspector URL
-}
-```
-
-## Event Dispatching
-
-**Fetch (no HTTP server):**
-```js
-const res = await mf.dispatchFetch("http://localhost:8787/path", {
- method: "POST",
- headers: { "Authorization": "Bearer token" },
- body: JSON.stringify({ data: "value" }),
-});
-```
-
-**Custom Host routing:**
-```js
-const res = await mf.dispatchFetch("http://localhost:8787/", {
- headers: { "Host": "api.example.com" },
-});
-```
-
-**Scheduled:**
-```js
-const worker = await mf.getWorker();
-const result = await worker.scheduled({ cron: "30 * * * *" });
-// result: { outcome: "ok", noRetry: false }
-```
-
-**Queue:**
-```js
-const worker = await mf.getWorker();
-const result = await worker.queue("queue-name", [
- { id: "msg1", timestamp: new Date(), body: "data", attempts: 1 },
-]);
-// result: { outcome: "ok", retryAll: false, ackAll: false, ... }
-```
-
-## Bindings Access
-
-**Environment variables:**
-```js
-// Basic usage
-const bindings = await mf.getBindings();
-console.log(bindings.SECRET_KEY);
-
-// With type safety (recommended):
-interface Env {
- SECRET_KEY: string;
- API_URL: string;
- KV: KVNamespace;
-}
-const env = await mf.getBindings();
-env.SECRET_KEY; // string (typed!)
-env.KV.get("key"); // KVNamespace methods available
-```
-
-**Request.cf object:**
-```js
-const cf = await mf.getCf();
-console.log(cf?.colo); // "DFW"
-console.log(cf?.country); // "US"
-```
-
-**KV:**
-```js
-const ns = await mf.getKVNamespace("TEST_NAMESPACE");
-await ns.put("key", "value");
-const value = await ns.get("key");
-```
-
-**R2:**
-```js
-const bucket = await mf.getR2Bucket("BUCKET");
-await bucket.put("file.txt", "content");
-const object = await bucket.get("file.txt");
-```
-
-**Durable Objects:**
-```js
-const ns = await mf.getDurableObjectNamespace("COUNTER");
-const id = ns.idFromName("test");
-const stub = ns.get(id);
-const res = await stub.fetch("http://localhost/");
-
-// Access storage directly:
-const storage = await mf.getDurableObjectStorage(id);
-await storage.put("key", "value");
-```
-
-**D1:**
-```js
-const db = await mf.getD1Database("DB");
-await db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`);
-await db.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice").run();
-```
-
-**Cache:**
-```js
-const caches = await mf.getCaches();
-const defaultCache = caches.default;
-await defaultCache.put("http://example.com", new Response("cached"));
-```
-
-**Queue producer:**
-```js
-const producer = await mf.getQueueProducer("QUEUE");
-await producer.send({ body: "message data" });
-```
-
-## Lifecycle
-
-**Reload:**
-```js
-await mf.setOptions({
- scriptPath: "worker.js",
- bindings: { VERSION: "2.0" },
-});
-```
-
-**Watch (manual):**
-```js
-import { watch } from "fs";
-
-const config = { scriptPath: "worker.js" };
-const mf = new Miniflare(config);
-
-watch("worker.js", async () => {
- console.log("Reloading...");
- await mf.setOptions(config);
-});
-```
-
-**Cleanup:**
-```js
-await mf.dispose();
-```
-
-## Debugging
-
-**Inspector URL for DevTools:**
-```js
-const url = await mf.getInspectorURL();
-console.log(`DevTools: ${url}`);
-// Open in Chrome DevTools for breakpoints, profiling
-```
-
-**Wait for server ready:**
-```js
-const mf = new Miniflare({ scriptPath: "worker.js" });
-const url = await mf.ready; // Promise
-console.log(`Server running at ${url}`); // http://127.0.0.1:8787
-
-// Note: dispatchFetch() waits automatically, no need to await ready
-const res = await mf.dispatchFetch("http://localhost/"); // Works immediately
-```
-
-See [configuration.md](./configuration.md) for all constructor options.
diff --git a/.agents/skills/cloudflare-deploy/references/miniflare/configuration.md b/.agents/skills/cloudflare-deploy/references/miniflare/configuration.md
deleted file mode 100644
index b269b24..0000000
--- a/.agents/skills/cloudflare-deploy/references/miniflare/configuration.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Configuration
-
-## Script Loading
-
-```js
-// Inline
-new Miniflare({ modules: true, script: `export default { ... }` });
-
-// File-based
-new Miniflare({ scriptPath: "worker.js" });
-
-// Multi-module
-new Miniflare({
- scriptPath: "src/index.js",
- modules: true,
- modulesRules: [
- { type: "ESModule", include: ["**/*.js"] },
- { type: "Text", include: ["**/*.txt"] },
- ],
-});
-```
-
-## Compatibility
-
-```js
-new Miniflare({
- compatibilityDate: "2026-01-01", // Use recent date for latest features
- compatibilityFlags: [
- "nodejs_compat", // Node.js APIs (process, Buffer, etc)
- "streams_enable_constructors", // Stream constructors
- ],
- upstream: "https://example.com", // Fallback for unhandled requests
-});
-```
-
-**Critical:** Use `compatibilityDate: "2026-01-01"` or latest to match production runtime. Old dates limit available APIs.
-
-## HTTP Server & Request.cf
-
-```js
-new Miniflare({
- port: 8787, // Default: 8787
- host: "127.0.0.1",
- https: true, // Self-signed cert
- liveReload: true, // Auto-reload HTML
-
- cf: true, // Fetch live Request.cf data (cached)
- // cf: "./cf.json", // Or load from file
- // cf: { colo: "DFW" }, // Or inline mock
-});
-```
-
-**Note:** For tests, use `dispatchFetch()` (no port conflicts).
-
-## Storage Bindings
-
-```js
-new Miniflare({
- // KV
- kvNamespaces: ["TEST_NAMESPACE", "CACHE"],
- kvPersist: "./kv-data", // Optional: persist to disk
-
- // R2
- r2Buckets: ["BUCKET", "IMAGES"],
- r2Persist: "./r2-data",
-
- // Durable Objects
- modules: true,
- durableObjects: {
- COUNTER: "Counter", // className
- API_OBJECT: { className: "ApiObject", scriptName: "api-worker" },
- },
- durableObjectsPersist: "./do-data",
-
- // D1
- d1Databases: ["DB"],
- d1Persist: "./d1-data",
-
- // Cache
- cache: true, // Default
- cachePersist: "./cache-data",
-});
-```
-
-## Bindings
-
-```js
-new Miniflare({
- // Environment variables
- bindings: {
- SECRET_KEY: "my-secret-value",
- API_URL: "https://api.example.com",
- DEBUG: true,
- },
-
- // Other bindings
- wasmBindings: { ADD_MODULE: "./add.wasm" },
- textBlobBindings: { TEXT: "./data.txt" },
- queueProducers: ["QUEUE"],
-});
-```
-
-## Multiple Workers
-
-```js
-new Miniflare({
- workers: [
- {
- name: "main",
- kvNamespaces: { DATA: "shared" },
- serviceBindings: { API: "api-worker" },
- script: `export default { ... }`,
- },
- {
- name: "api-worker",
- kvNamespaces: { DATA: "shared" }, // Shared storage
- script: `export default { ... }`,
- },
- ],
-});
-```
-
-**With routing:**
-```js
-workers: [
- { name: "api", scriptPath: "./api.js", routes: ["api.example.com/*"] },
- { name: "web", scriptPath: "./web.js", routes: ["example.com/*"] },
-],
-```
-
-## Logging & Performance
-
-```js
-import { Log, LogLevel } from "miniflare";
-
-new Miniflare({
- log: new Log(LogLevel.DEBUG), // DEBUG | INFO | WARN | ERROR | NONE
- scriptTimeout: 30000, // CPU limit (ms)
- workersConcurrencyLimit: 10, // Max concurrent workers
-});
-```
-
-## Workers Sites
-
-```js
-new Miniflare({
- sitePath: "./public",
- siteInclude: ["**/*.html", "**/*.css"],
- siteExclude: ["**/*.map"],
-});
-```
-
-## From wrangler.toml
-
-Miniflare doesn't auto-read `wrangler.toml`:
-
-```toml
-# wrangler.toml
-name = "my-worker"
-main = "src/index.ts"
-compatibility_date = "2026-01-01"
-[[kv_namespaces]]
-binding = "KV"
-```
-
-```js
-// Miniflare equivalent
-new Miniflare({
- scriptPath: "src/index.ts",
- compatibilityDate: "2026-01-01",
- kvNamespaces: ["KV"],
-});
-```
diff --git a/.agents/skills/cloudflare-deploy/references/miniflare/gotchas.md b/.agents/skills/cloudflare-deploy/references/miniflare/gotchas.md
deleted file mode 100644
index dfcd157..0000000
--- a/.agents/skills/cloudflare-deploy/references/miniflare/gotchas.md
+++ /dev/null
@@ -1,160 +0,0 @@
-# Gotchas & Troubleshooting
-
-## Miniflare Limitations
-
-**Not supported:**
-- Analytics Engine (use mocks)
-- Cloudflare Images/Stream
-- Browser Rendering API
-- Tail Workers
-- Workers for Platforms (partial support)
-
-**Behavior differences from production:**
-- Runs workerd locally, not Cloudflare edge
-- Storage is local (filesystem/memory), not distributed
-- `Request.cf` is cached/mocked, not real edge data
-- Performance differs from edge
-- Caching implementation may vary slightly
-
-## Common Errors
-
-### "Cannot find module"
-**Cause:** Module path wrong or `modulesRules` not configured
-**Solution:**
-```js
-new Miniflare({
- modules: true,
- modulesRules: [{ type: "ESModule", include: ["**/*.js"] }],
-});
-```
-
-### "Data not persisting"
-**Cause:** Persist paths are files, not directories
-**Solution:**
-```js
-kvPersist: "./data/kv", // Directory, not file
-```
-
-### "Cannot run TypeScript"
-**Cause:** Miniflare doesn't transpile TypeScript
-**Solution:** Build first with esbuild/tsc, then run compiled JS
-
-### "`request.cf` is undefined"
-**Cause:** CF data not configured
-**Solution:**
-```js
-new Miniflare({ cf: true }); // Or cf: "./cf.json"
-```
-
-### "EADDRINUSE" port conflict
-**Cause:** Multiple instances using same port
-**Solution:** Use `dispatchFetch()` (no HTTP server) or `port: 0` for auto-assign
-
-### "Durable Object not found"
-**Cause:** Class export doesn't match config name
-**Solution:**
-```js
-export class Counter {} // Must match
-new Miniflare({ durableObjects: { COUNTER: "Counter" } });
-```
-
-## Debugging
-
-**Enable verbose logging:**
-```js
-import { Log, LogLevel } from "miniflare";
-new Miniflare({ log: new Log(LogLevel.DEBUG) });
-```
-
-**Chrome DevTools:**
-```js
-const url = await mf.getInspectorURL();
-console.log(`DevTools: ${url}`); // Open in Chrome
-```
-
-**Inspect bindings:**
-```js
-const env = await mf.getBindings();
-console.log(Object.keys(env));
-```
-
-**Verify storage:**
-```js
-const ns = await mf.getKVNamespace("TEST");
-const { keys } = await ns.list();
-```
-
-## Best Practices
-
-**✓ Do:**
-- Use `dispatchFetch()` for tests (no HTTP server)
-- In-memory storage for CI (omit persist options)
-- New instances per test for isolation
-- Type-safe bindings with interfaces
-- `await mf.dispose()` in cleanup
-
-**✗ Avoid:**
-- HTTP server in tests
-- Shared instances without cleanup
-- Old compatibility dates (use 2026+)
-
-## Migration Guides
-
-### From Miniflare 2.x to 3+
-
-Breaking changes in v3+:
-
-| v2 | v3+ |
-|----|-----|
-| `getBindings()` sync | `getBindings()` returns Promise |
-| `ready` is void | `ready` returns `Promise` |
-| service-worker-mock | Built on workerd |
-| Different options | Restructured constructor |
-
-**Example migration:**
-```js
-// v2
-const bindings = mf.getBindings();
-mf.ready; // void
-
-// v3+
-const bindings = await mf.getBindings();
-const url = await mf.ready; // Promise
-```
-
-### From unstable_dev to Miniflare
-
-```js
-// Old (deprecated)
-import { unstable_dev } from "wrangler";
-const worker = await unstable_dev("src/index.ts");
-
-// New
-import { Miniflare } from "miniflare";
-const mf = new Miniflare({ scriptPath: "src/index.ts" });
-```
-
-### From Wrangler Dev
-
-Miniflare doesn't auto-read `wrangler.toml`:
-
-```js
-// Translate manually:
-new Miniflare({
- scriptPath: "dist/worker.js",
- compatibilityDate: "2026-01-01",
- kvNamespaces: ["KV"],
- bindings: { API_KEY: process.env.API_KEY },
-});
-```
-
-## Resource Limits
-
-| Limit | Value | Notes |
-|-------|-------|-------|
-| CPU time | 30s default | Configurable via `scriptTimeout` |
-| Storage | Filesystem | Performance varies by disk |
-| Memory | System dependent | No artificial limits |
-| Request.cf | Cached/mocked | Not live edge data |
-
-See [patterns.md](./patterns.md) for testing examples.
diff --git a/.agents/skills/cloudflare-deploy/references/miniflare/patterns.md b/.agents/skills/cloudflare-deploy/references/miniflare/patterns.md
deleted file mode 100644
index c89c3a5..0000000
--- a/.agents/skills/cloudflare-deploy/references/miniflare/patterns.md
+++ /dev/null
@@ -1,181 +0,0 @@
-# Testing Patterns
-
-## Choosing a Testing Approach
-
-| Approach | Use Case | Speed | Setup | Runtime |
-|----------|----------|-------|-------|---------|
-| **getPlatformProxy** | Unit tests, logic testing | Fast | Low | Miniflare |
-| **Miniflare API** | Integration tests, full control | Medium | Medium | Miniflare |
-| **vitest-pool-workers** | Vitest runner integration | Medium | Medium | workerd |
-
-**Quick guide:**
-- Unit tests → getPlatformProxy
-- Integration tests → Miniflare API
-- Vitest workflows → vitest-pool-workers
-
-## getPlatformProxy
-
-Lightweight unit testing - provides bindings without full Worker runtime.
-
-```js
-// vitest.config.js
-export default { test: { environment: "node" } };
-```
-
-```js
-import { env } from "cloudflare:test";
-import { describe, it, expect } from "vitest";
-
-describe("Business logic", () => {
- it("processes data with KV", async () => {
- await env.KV.put("test", "value");
- expect(await env.KV.get("test")).toBe("value");
- });
-});
-```
-
-**Pros:** Fast, simple
-**Cons:** No full runtime, can't test fetch handler
-
-## vitest-pool-workers
-
-Full Workers runtime in Vitest. Reads `wrangler.toml`.
-
-```bash
-npm i -D @cloudflare/vitest-pool-workers
-```
-
-```js
-// vitest.config.js
-import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
-
-export default defineWorkersConfig({
- test: {
- poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" } } },
- },
-});
-```
-
-```js
-import { env, SELF } from "cloudflare:test";
-import { it, expect } from "vitest";
-
-it("handles fetch", async () => {
- const res = await SELF.fetch("http://example.com/");
- expect(res.status).toBe(200);
-});
-```
-
-**Pros:** Full runtime, uses wrangler.toml
-**Cons:** Requires Wrangler config
-
-## Miniflare API (node:test)
-
-```js
-import assert from "node:assert";
-import test, { after, before } from "node:test";
-import { Miniflare } from "miniflare";
-
-let mf;
-before(() => {
- mf = new Miniflare({ scriptPath: "src/index.js", kvNamespaces: ["TEST_KV"] });
-});
-
-test("fetch", async () => {
- const res = await mf.dispatchFetch("http://localhost/");
- assert.strictEqual(await res.text(), "Hello");
-});
-
-after(() => mf.dispose());
-```
-
-## Testing Durable Objects & Events
-
-```js
-// Durable Objects
-const ns = await mf.getDurableObjectNamespace("COUNTER");
-const stub = ns.get(ns.idFromName("test-counter"));
-await stub.fetch("http://localhost/increment");
-
-// Direct storage
-const storage = await mf.getDurableObjectStorage(ns.idFromName("test-counter"));
-const count = await storage.get("count");
-
-// Queue
-const worker = await mf.getWorker();
-await worker.queue("my-queue", [
- { id: "msg1", timestamp: new Date(), body: { userId: 123 }, attempts: 1 },
-]);
-
-// Scheduled
-await worker.scheduled({ cron: "0 0 * * *" });
-```
-
-## Test Isolation & Mocking
-
-```js
-// Per-test isolation
-beforeEach(() => { mf = new Miniflare({ kvNamespaces: ["TEST"] }); });
-afterEach(() => mf.dispose());
-
-// Mock external APIs
-new Miniflare({
- workers: [
- { name: "main", serviceBindings: { API: "mock-api" }, script: `...` },
- { name: "mock-api", script: `export default { async fetch() { return Response.json({mock: true}); } }` },
- ],
-});
-```
-
-## Type Safety
-
-```ts
-import type { KVNamespace } from "@cloudflare/workers-types";
-
-interface Env {
- KV: KVNamespace;
- API_KEY: string;
-}
-
-const env = await mf.getBindings();
-await env.KV.put("key", "value"); // Typed!
-
-export default {
- async fetch(req: Request, env: Env) {
- return new Response(await env.KV.get("key"));
- }
-} satisfies ExportedHandler;
-```
-
-## WebSocket Testing
-
-```js
-const res = await mf.dispatchFetch("http://localhost/ws", {
- headers: { Upgrade: "websocket" },
-});
-assert.strictEqual(res.status, 101);
-```
-
-## Migration from unstable_dev
-
-```js
-// Old (deprecated)
-import { unstable_dev } from "wrangler";
-const worker = await unstable_dev("src/index.ts");
-
-// New
-import { Miniflare } from "miniflare";
-const mf = new Miniflare({ scriptPath: "src/index.ts" });
-```
-
-## CI/CD Tips
-
-```js
-// In-memory storage (faster)
-new Miniflare({ kvNamespaces: ["TEST"] }); // No persist = in-memory
-
-// Use dispatchFetch (no port conflicts)
-await mf.dispatchFetch("http://localhost/");
-```
-
-See [gotchas.md](./gotchas.md) for troubleshooting.
diff --git a/.agents/skills/cloudflare-deploy/references/network-interconnect/README.md b/.agents/skills/cloudflare-deploy/references/network-interconnect/README.md
deleted file mode 100644
index e337f1b..0000000
--- a/.agents/skills/cloudflare-deploy/references/network-interconnect/README.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Cloudflare Network Interconnect (CNI)
-
-Private, high-performance connectivity to Cloudflare's network. **Enterprise-only**.
-
-## Connection Types
-
-**Direct**: Physical fiber in shared datacenter. 10/100 Gbps. You order cross-connect.
-
-**Partner**: Virtual via Console Connect, Equinix, Megaport, etc. Managed via partner SDN.
-
-**Cloud**: AWS Direct Connect or GCP Cloud Interconnect. Magic WAN only.
-
-## Dataplane Versions
-
-**v1 (Classic)**: GRE tunnel support, VLAN/BFD/LACP, asymmetric MTU (1500↓/1476↑), peering support.
-
-**v2 (Beta)**: No GRE, 1500 MTU both ways, no VLAN/BFD/LACP yet, ECMP instead.
-
-## Use Cases
-
-- **Magic Transit DSR**: DDoS protection, egress via ISP (v1/v2)
-- **Magic Transit + Egress**: DDoS + egress via CF (v1/v2)
-- **Magic WAN + Zero Trust**: Private backbone (v1 needs GRE, v2 native)
-- **Peering**: Public routes at PoP (v1 only)
-- **App Security**: WAF/Cache/LB (v1/v2 over Magic Transit)
-
-## Prerequisites
-
-- Enterprise plan
-- IPv4 /24+ or IPv6 /48+ prefixes
-- BGP ASN for v1
-- See [locations PDF](https://developers.cloudflare.com/network-interconnect/static/cni-locations-2026-01.pdf)
-
-## Specs
-
-- /31 point-to-point subnets
-- 10km max optical distance
-- 10G: 10GBASE-LR single-mode
-- 100G: 100GBASE-LR4 single-mode
-- **No SLA** (free service)
-- Backup Internet required
-
-## Throughput
-
-| Direction | 10G | 100G |
-|-----------|-----|------|
-| CF → Customer | 10 Gbps | 100 Gbps |
-| Customer → CF (peering) | 10 Gbps | 100 Gbps |
-| Customer → CF (Magic) | 1 Gbps/tunnel or CNI | 1 Gbps/tunnel or CNI |
-
-## Timeline
-
-2-4 weeks typical. Steps: request → config review → order connection → configure → test → enable health checks → activate → monitor.
-
-## In This Reference
-- [configuration.md](./configuration.md) - BGP, routing, setup
-- [api.md](./api.md) - API endpoints, SDKs
-- [patterns.md](./patterns.md) - HA, hybrid cloud, failover
-- [gotchas.md](./gotchas.md) - Troubleshooting, limits
-
-## Reading Order by Task
-
-| Task | Files to Load |
-|------|---------------|
-| Initial setup | README → configuration.md → api.md |
-| Create interconnect via API | api.md → gotchas.md |
-| Design HA architecture | patterns.md → README |
-| Troubleshoot connection | gotchas.md → configuration.md |
-| Cloud integration (AWS/GCP) | configuration.md → patterns.md |
-| Monitor + alerts | configuration.md |
-
-## Automation Boundary
-
-**API-Automatable:**
-- List/create/delete interconnects (Direct, Partner)
-- List available slots
-- Get interconnect status
-- Download LOA PDF
-- Create/update CNI objects (BGP config)
-- Query settings
-
-**Requires Account Team:**
-- Initial request approval
-- AWS Direct Connect setup (send LOA+VLAN to CF)
-- GCP Cloud Interconnect final activation
-- Partner interconnect acceptance (Equinix, Megaport)
-- VLAN assignment (v1)
-- Configuration document generation (v1)
-- Escalations + troubleshooting support
-
-**Cannot Be Automated:**
-- Physical cross-connect installation (Direct)
-- Partner portal operations (virtual circuit ordering)
-- AWS/GCP portal operations
-- Maintenance window coordination
-
-## See Also
-- [tunnel](../tunnel/) - Alternative for private network connectivity
-- [spectrum](../spectrum/) - Layer 4 proxy for TCP/UDP traffic
diff --git a/.agents/skills/cloudflare-deploy/references/network-interconnect/api.md b/.agents/skills/cloudflare-deploy/references/network-interconnect/api.md
deleted file mode 100644
index 85e5e12..0000000
--- a/.agents/skills/cloudflare-deploy/references/network-interconnect/api.md
+++ /dev/null
@@ -1,199 +0,0 @@
-# CNI API Reference
-
-See [README.md](README.md) for overview.
-
-## Base
-
-```
-https://api.cloudflare.com/client/v4
-Auth: Authorization: Bearer
-```
-
-## SDK Namespaces
-
-**Primary (recommended):**
-```typescript
-client.networkInterconnects.interconnects.*
-client.networkInterconnects.cnis.*
-client.networkInterconnects.slots.*
-```
-
-**Alternate (deprecated):**
-```typescript
-client.magicTransit.cfInterconnects.*
-```
-
-Use `networkInterconnects` namespace for all new code.
-
-## Interconnects
-
-```http
-GET /accounts/{account_id}/cni/interconnects # Query: page, per_page
-POST /accounts/{account_id}/cni/interconnects # Query: validate_only=true (optional)
-GET /accounts/{account_id}/cni/interconnects/{icon}
-GET /accounts/{account_id}/cni/interconnects/{icon}/status
-GET /accounts/{account_id}/cni/interconnects/{icon}/loa # Returns PDF
-DELETE /accounts/{account_id}/cni/interconnects/{icon}
-```
-
-**Create Body:** `account`, `slot_id`, `type`, `facility`, `speed`, `name`, `description`
-**Status Values:** `active` | `healthy` | `unhealthy` | `pending` | `down`
-
-**Response Example:**
-```json
-{"result": [{"id": "icon_abc", "name": "prod", "type": "direct", "facility": "EWR1", "speed": "10G", "status": "active"}]}
-```
-
-## CNI Objects (BGP config)
-
-```http
-GET /accounts/{account_id}/cni/cnis
-POST /accounts/{account_id}/cni/cnis
-GET /accounts/{account_id}/cni/cnis/{cni}
-PUT /accounts/{account_id}/cni/cnis/{cni}
-DELETE /accounts/{account_id}/cni/cnis/{cni}
-```
-
-Body: `account`, `cust_ip`, `cf_ip`, `bgp_asn`, `bgp_password`, `vlan`
-
-## Slots
-
-```http
-GET /accounts/{account_id}/cni/slots
-GET /accounts/{account_id}/cni/slots/{slot}
-```
-
-Query: `facility`, `occupied`, `speed`
-
-## Health Checks
-
-Configure via Magic Transit/WAN tunnel endpoints (CNI v2).
-
-```typescript
-await client.magicTransit.tunnels.update(accountId, tunnelId, {
- health_check: { enabled: true, target: '192.0.2.1', rate: 'high', type: 'request' },
-});
-```
-
-Rates: `high` | `medium` | `low`. Types: `request` | `reply`. See [Magic Transit docs](https://developers.cloudflare.com/magic-transit/how-to/configure-tunnel-endpoints/#add-tunnels).
-
-## Settings
-
-```http
-GET /accounts/{account_id}/cni/settings
-PUT /accounts/{account_id}/cni/settings
-```
-
-Body: `default_asn`
-
-## TypeScript SDK
-
-```typescript
-import Cloudflare from 'cloudflare';
-
-const client = new Cloudflare({ apiToken: process.env.CF_TOKEN });
-
-// List
-await client.networkInterconnects.interconnects.list({ account_id: id });
-
-// Create with validation
-await client.networkInterconnects.interconnects.create({
- account_id: id,
- account: id,
- slot_id: 'slot_abc',
- type: 'direct',
- facility: 'EWR1',
- speed: '10G',
- name: 'prod-interconnect',
-}, {
- query: { validate_only: true }, // Dry-run validation
-});
-
-// Create without validation
-await client.networkInterconnects.interconnects.create({
- account_id: id,
- account: id,
- slot_id: 'slot_abc',
- type: 'direct',
- facility: 'EWR1',
- speed: '10G',
- name: 'prod-interconnect',
-});
-
-// Status
-await client.networkInterconnects.interconnects.get(accountId, iconId);
-
-// LOA (use fetch)
-const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${id}/cni/interconnects/${iconId}/loa`, {
- headers: { Authorization: `Bearer ${token}` },
-});
-await fs.writeFile('loa.pdf', Buffer.from(await res.arrayBuffer()));
-
-// CNI object
-await client.networkInterconnects.cnis.create({
- account_id: id,
- account: id,
- cust_ip: '192.0.2.1/31',
- cf_ip: '192.0.2.0/31',
- bgp_asn: 65000,
- vlan: 100,
-});
-
-// Slots (filter by facility and speed)
-await client.networkInterconnects.slots.list({
- account_id: id,
- occupied: false,
- facility: 'EWR1',
- speed: '10G',
-});
-```
-
-## Python SDK
-
-```python
-from cloudflare import Cloudflare
-
-client = Cloudflare(api_token=os.environ["CF_TOKEN"])
-
-# List, create, status (same pattern as TypeScript)
-client.network_interconnects.interconnects.list(account_id=id)
-client.network_interconnects.interconnects.create(account_id=id, account=id, slot_id="slot_abc", type="direct", facility="EWR1", speed="10G")
-client.network_interconnects.interconnects.get(account_id=id, icon=icon_id)
-
-# CNI objects and slots
-client.network_interconnects.cnis.create(account_id=id, cust_ip="192.0.2.1/31", cf_ip="192.0.2.0/31", bgp_asn=65000)
-client.network_interconnects.slots.list(account_id=id, occupied=False)
-```
-
-## cURL
-
-```bash
-# List interconnects
-curl "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects" \
- -H "Authorization: Bearer ${CF_TOKEN}"
-
-# Create interconnect
-curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects?validate_only=true" \
- -H "Authorization: Bearer ${CF_TOKEN}" -H "Content-Type: application/json" \
- -d '{"account": "id", "slot_id": "slot_abc", "type": "direct", "facility": "EWR1", "speed": "10G"}'
-
-# LOA PDF
-curl "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cni/interconnects/${ICON_ID}/loa" \
- -H "Authorization: Bearer ${CF_TOKEN}" --output loa.pdf
-```
-
-## Not Available via API
-
-**Missing Capabilities:**
-- BGP session state query (use Dashboard or BGP logs)
-- Bandwidth utilization metrics (use external monitoring)
-- Traffic statistics per interconnect
-- Historical uptime/downtime data
-- Light level readings (contact account team)
-- Maintenance window scheduling (notifications only)
-
-## Resources
-
-- [API Docs](https://developers.cloudflare.com/api/resources/network_interconnects/)
-- [TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)
-- [Python SDK](https://github.com/cloudflare/cloudflare-python)
diff --git a/.agents/skills/cloudflare-deploy/references/network-interconnect/configuration.md b/.agents/skills/cloudflare-deploy/references/network-interconnect/configuration.md
deleted file mode 100644
index 0f1005c..0000000
--- a/.agents/skills/cloudflare-deploy/references/network-interconnect/configuration.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# CNI Configuration
-
-See [README.md](README.md) for overview.
-
-## Workflow (2-4 weeks)
-
-1. **Submit request** (Week 1): Contact account team, provide type/location/use case
-2. **Review config** (Week 1-2, v1 only): Approve IP/VLAN/spec doc
-3. **Order connection** (Week 2-3):
- - **Direct**: Get LOA, order cross-connect from facility
- - **Partner**: Order virtual circuit in partner portal
- - **Cloud**: Order Direct Connect/Cloud Interconnect, send LOA+VLAN to CF
-4. **Configure** (Week 3): Both sides configure per doc
-5. **Test** (Week 3-4): Ping, verify BGP, check routes
-6. **Health checks** (Week 4): Configure [Magic Transit](https://developers.cloudflare.com/magic-transit/how-to/configure-tunnel-endpoints/#add-tunnels) or [Magic WAN](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-tunnel-endpoints/#add-tunnels) health checks
-7. **Activate** (Week 4): Route traffic, verify flow
-8. **Monitor**: Enable [maintenance notifications](https://developers.cloudflare.com/network-interconnect/monitoring-and-alerts/#enable-cloudflare-status-maintenance-notification)
-
-## BGP Configuration
-
-**v1 Requirements:**
-- BGP ASN (provide during setup)
-- /31 subnet for peering
-- Optional: BGP password
-
-**v2:** Simplified, less BGP config needed.
-
-**BGP over CNI (Dec 2024):** Magic WAN/Transit can now peer BGP directly over CNI v2 (no GRE tunnel required).
-
-**Example v1 BGP:**
-```
-Router ID: 192.0.2.1
-Peer IP: 192.0.2.0
-Remote ASN: 13335
-Local ASN: 65000
-Password: [optional]
-VLAN: 100
-```
-
-## Cloud Interconnect Setup
-
-### AWS Direct Connect (Beta)
-
-**Requirements:** Magic WAN, AWS Dedicated Direct Connect 1/10 Gbps.
-
-**Process:**
-1. Contact CF account team
-2. Choose location
-3. Order in AWS portal
-4. AWS provides LOA + VLAN ID
-5. Send to CF account team
-6. Wait ~4 weeks
-
-**Post-setup:** Add [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes) to Magic WAN. Enable [bidirectional health checks](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-tunnel-endpoints/#legacy-bidirectional-health-checks).
-
-### GCP Cloud Interconnect (Beta)
-
-**Setup via Dashboard:**
-1. Interconnects → Create → Cloud Interconnect → Google
-2. Provide name, MTU (match GCP VLAN attachment), speed (50M-50G granular options available for partner interconnects)
-3. Enter VLAN attachment pairing key
-4. Confirm order
-
-**Routing to GCP:** Add [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes). BGP routes from GCP Cloud Router **ignored**.
-
-**Routing to CF:** Configure [custom learned routes](https://cloud.google.com/network-connectivity/docs/router/how-to/configure-custom-learned-routes) in Cloud Router. Request prefixes from CF account team.
-
-## Monitoring
-
-**Dashboard Status:**
-
-| Status | Meaning |
-|--------|---------|
-| **Healthy** | Link operational, traffic flowing, health checks passing |
-| **Active** | Link up, sufficient light, Ethernet negotiated |
-| **Unhealthy** | Link down, no/low light (<-20 dBm), can't negotiate |
-| **Pending** | Cross-connect incomplete, device unresponsive, RX/TX swapped |
-| **Down** | Physical link down, no connectivity |
-
-**Alerts:**
-
-**CNI Connection Maintenance** (Magic Networking only):
-```
-Dashboard → Notifications → Add
-Product: Cloudflare Network Interconnect
-Type: Connection Maintenance Alert
-```
-Warnings up to 2 weeks advance. 6hr delay for new additions.
-
-**Cloudflare Status Maintenance** (entire PoP):
-```
-Dashboard → Notifications → Add
-Product: Cloudflare Status
-Filter PoPs: gru,fra,lhr
-```
-
-**Find PoP code:**
-```
-Dashboard → Magic Transit/WAN → Configuration → Interconnects
-Select CNI → Note Data Center (e.g., "gru-b")
-Use first 3 letters: "gru"
-```
-
-## Best Practices
-
-**Critical config-specific practices:**
-- /31 subnets required for BGP
-- BGP passwords recommended
-- BFD for fast failover (v1 only)
-- Test ping connectivity before BGP
-- Enable maintenance notifications immediately after activation
-- Monitor status programmatically via API
-
-For design patterns, HA architecture, and security best practices, see [patterns.md](./patterns.md).
diff --git a/.agents/skills/cloudflare-deploy/references/network-interconnect/gotchas.md b/.agents/skills/cloudflare-deploy/references/network-interconnect/gotchas.md
deleted file mode 100644
index 9880807..0000000
--- a/.agents/skills/cloudflare-deploy/references/network-interconnect/gotchas.md
+++ /dev/null
@@ -1,165 +0,0 @@
-# CNI Gotchas & Troubleshooting
-
-## Common Errors
-
-### "Status: Pending"
-
-**Cause:** Cross-connect not installed, RX/TX fibers reversed, wrong fiber type, or low light levels
-**Solution:**
-1. Verify cross-connect installed
-2. Check fiber at patch panel
-3. Swap RX/TX fibers
-4. Check light with optical power meter (target > -20 dBm)
-5. Contact account team
-
-### "Status: Unhealthy"
-
-**Cause:** Physical issue, low light (<-20 dBm), optic mismatch, or dirty connectors
-**Solution:**
-1. Check physical connections
-2. Clean fiber connectors
-3. Verify optic types (10GBASE-LR/100GBASE-LR4)
-4. Test with known-good optics
-5. Check patch panel
-6. Contact account team
-
-### "BGP Session Down"
-
-**Cause:** Wrong IP addressing, wrong ASN, password mismatch, or firewall blocking TCP/179
-**Solution:**
-1. Verify IPs match CNI object
-2. Confirm ASN correct
-3. Check BGP password
-4. Verify no firewall on TCP/179
-5. Check BGP logs
-6. Review BGP timers
-
-### "Low Throughput"
-
-**Cause:** MTU mismatch, fragmentation, single GRE tunnel (v1), or routing inefficiency
-**Solution:**
-1. Check MTU (1500↓/1476↑ for v1, 1500 both for v2)
-2. Test various packet sizes
-3. Add more GRE tunnels (v1)
-4. Consider upgrading to v2
-5. Review routing tables
-6. Use LACP for bundling (v1)
-
-## API Errors
-
-### 400 Bad Request: "slot_id already occupied"
-
-**Cause:** Another interconnect already uses this slot
-**Solution:** Use `occupied=false` filter when listing slots:
-```typescript
-await client.networkInterconnects.slots.list({
- account_id: id,
- occupied: false,
- facility: 'EWR1',
-});
-```
-
-### 400 Bad Request: "invalid facility code"
-
-**Cause:** Typo or unsupported facility
-**Solution:** Check [locations PDF](https://developers.cloudflare.com/network-interconnect/static/cni-locations-2026-01.pdf) for valid codes
-
-### 403 Forbidden: "Enterprise plan required"
-
-**Cause:** Account not enterprise-level
-**Solution:** Contact account team to upgrade
-
-### 422 Unprocessable: "validate_only request failed"
-
-**Cause:** Dry-run validation found issues (wrong slot, invalid config)
-**Solution:** Review error message details, fix config before real creation
-
-### Rate Limiting
-
-**Limit:** 1200 requests/5min per token
-**Solution:** Implement exponential backoff, cache slot listings
-
-## Cloud-Specific Issues
-
-### AWS Direct Connect: "VLAN not matching"
-
-**Cause:** VLAN ID from AWS LOA doesn't match CNI config
-**Solution:**
-1. Get VLAN from AWS Console after ordering
-2. Send exact VLAN to CF account team
-3. Verify match in CNI object config
-
-### AWS: "Connection stuck in Pending"
-
-**Cause:** LOA not provided to CF or AWS connection not accepted
-**Solution:**
-1. Verify AWS connection status is "Available"
-2. Confirm LOA sent to CF account team
-3. Wait for CF team acceptance (can take days)
-
-### GCP: "BGP routes not propagating"
-
-**Cause:** BGP routes from GCP Cloud Router **ignored by design**
-**Solution:** Use [static routes](https://developers.cloudflare.com/magic-wan/configuration/manually/how-to/configure-routes/#configure-static-routes) in Magic WAN instead
-
-### GCP: "Cannot query VLAN attachment status via API"
-
-**Cause:** GCP Cloud Interconnect Dashboard-only (no API yet)
-**Solution:** Check status in CF Dashboard or GCP Console
-
-## Partner Interconnect Issues
-
-### Equinix: "Virtual circuit not appearing"
-
-**Cause:** CF hasn't accepted Equinix connection request
-**Solution:**
-1. Verify VC created in Equinix Fabric Portal
-2. Contact CF account team to accept
-3. Allow 2-3 business days
-
-### Console Connect/Megaport: "API creation fails"
-
-**Cause:** Partner interconnects require partner portal + CF approval
-**Solution:** Cannot fully automate. Order in partner portal, notify CF account team.
-
-## Anti-Patterns
-
-| Anti-Pattern | Why Bad | Solution |
-|--------------|---------|----------|
-| Single interconnect for production | No SLA, single point of failure | Use ≥2 with device diversity |
-| No backup Internet | CNI fails = total outage | Always maintain alternate path |
-| Polling status every second | Rate limits, wastes API calls | Poll every 30-60s max |
-| Using v1 for Magic WAN v2 workloads | GRE overhead, complexity | Use v2 for simplified routing |
-| Assuming BGP session = traffic flowing | BGP up ≠ routes installed | Verify routing tables + test traffic |
-| Not enabling maintenance alerts | Surprise downtime during maintenance | Enable notifications immediately |
-| Hardcoding VLAN in automation | VLAN assigned by CF (v1) | Get VLAN from CNI object response |
-| Using Direct without colocation | Can't access cross-connect | Use Partner or Cloud interconnect |
-
-## What's Not Queryable via API
-
-**Cannot retrieve:**
-- BGP session state (use Dashboard or BGP logs)
-- Light levels (contact account team)
-- Historical metrics (uptime, traffic)
-- Bandwidth utilization per interconnect
-- Maintenance window schedules (notifications only)
-- Fiber path details
-- Cross-connect installation status
-
-**Workarounds:**
-- External monitoring for BGP state
-- Log aggregation for historical data
-- Notifications for maintenance windows
-
-## Limits
-
-| Resource/Limit | Value | Notes |
-|----------------|-------|-------|
-| Max optical distance | 10km | Physical limit |
-| MTU (v1) | 1500↓ / 1476↑ | Asymmetric |
-| MTU (v2) | 1500 both | Symmetric |
-| GRE tunnel throughput | 1 Gbps | Per tunnel (v1) |
-| Recovery time | Days | No formal SLA |
-| Light level minimum | -20 dBm | Target threshold |
-| API rate limit | 1200 req/5min | Per token |
-| Health check delay | 6 hours | New maintenance alert subscriptions |
diff --git a/.agents/skills/cloudflare-deploy/references/network-interconnect/patterns.md b/.agents/skills/cloudflare-deploy/references/network-interconnect/patterns.md
deleted file mode 100644
index 7ff9dd3..0000000
--- a/.agents/skills/cloudflare-deploy/references/network-interconnect/patterns.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# CNI Patterns
-
-See [README.md](README.md) for overview.
-
-## High Availability
-
-**Critical:** Design for resilience from day one.
-
-**Requirements:**
-- Device-level diversity (separate hardware)
-- Backup Internet connectivity (no SLA on CNI)
-- Network-resilient locations preferred
-- Regular failover testing
-
-**Architecture:**
-```
-Your Network A ──10G CNI v2──> CF CCR Device 1
- │
-Your Network B ──10G CNI v2──> CF CCR Device 2
- │
- CF Global Network (AS13335)
-```
-
-**Capacity Planning:**
-- Plan across all links
-- Account for failover scenarios
-- Your responsibility
-
-## Pattern: Magic Transit + CNI v2
-
-**Use Case:** DDoS protection, private connectivity, no GRE overhead.
-
-```typescript
-// 1. Create interconnect
-const ic = await client.networkInterconnects.interconnects.create({
- account_id: id,
- type: 'direct',
- facility: 'EWR1',
- speed: '10G',
- name: 'magic-transit-primary',
-});
-
-// 2. Poll until active
-const status = await pollUntilActive(id, ic.id);
-
-// 3. Configure Magic Transit tunnel via Dashboard/API
-```
-
-**Benefits:** 1500 MTU both ways, simplified routing.
-
-## Pattern: Multi-Cloud Hybrid
-
-**Use Case:** AWS/GCP workloads with Cloudflare.
-
-**AWS Direct Connect:**
-```typescript
-// 1. Order Direct Connect in AWS Console
-// 2. Get LOA + VLAN from AWS
-// 3. Send to CF account team (no API)
-// 4. Configure static routes in Magic WAN
-
-await configureStaticRoutes(id, {
- prefix: '10.0.0.0/8',
- nexthop: 'aws-direct-connect',
-});
-```
-
-**GCP Cloud Interconnect:**
-```
-1. Get VLAN attachment pairing key from GCP Console
-2. Create via Dashboard: Interconnects → Create → Cloud Interconnect → Google
- - Enter pairing key, name, MTU, speed
-3. Configure static routes in Magic WAN (BGP routes from GCP ignored)
-4. Configure custom learned routes in GCP Cloud Router
-```
-
-**Note:** Dashboard-only. No API/SDK support yet.
-
-## Pattern: Multi-Location HA
-
-**Use Case:** 99.99%+ uptime.
-
-```typescript
-// Primary (NY)
-const primary = await client.networkInterconnects.interconnects.create({
- account_id: id,
- type: 'direct',
- facility: 'EWR1',
- speed: '10G',
- name: 'primary-ewr1',
-});
-
-// Secondary (NY, different hardware)
-const secondary = await client.networkInterconnects.interconnects.create({
- account_id: id,
- type: 'direct',
- facility: 'EWR2',
- speed: '10G',
- name: 'secondary-ewr2',
-});
-
-// Tertiary (LA, different geography)
-const tertiary = await client.networkInterconnects.interconnects.create({
- account_id: id,
- type: 'partner',
- facility: 'LAX1',
- speed: '10G',
- name: 'tertiary-lax1',
-});
-
-// BGP local preferences:
-// Primary: 200
-// Secondary: 150
-// Tertiary: 100
-// Internet: Last resort
-```
-
-## Pattern: Partner Interconnect (Equinix)
-
-**Use Case:** Quick deployment, no colocation.
-
-**Setup:**
-1. Order virtual circuit in Equinix Fabric Portal
-2. Select Cloudflare as destination
-3. Choose facility
-4. Send details to CF account team
-5. CF accepts in portal
-6. Configure BGP
-
-**No API automation** – partner portals managed separately.
-
-## Failover & Security
-
-**Failover Best Practices:**
-- Use BGP local preferences for priority
-- Configure BFD for fast detection (v1)
-- Test regularly with traffic shift
-- Document runbooks
-
-**Security:**
-- BGP password authentication
-- BGP route filtering
-- Monitor unexpected routes
-- Magic Firewall for DDoS/threats
-- Minimum API token permissions
-- Rotate credentials periodically
-
-## Decision Matrix
-
-| Requirement | Recommended |
-|-------------|-------------|
-| Collocated with CF | Direct |
-| Not collocated | Partner |
-| AWS/GCP workloads | Cloud |
-| 1500 MTU both ways | v2 |
-| VLAN tagging | v1 |
-| Public peering | v1 |
-| Simplest config | v2 |
-| BFD fast failover | v1 |
-| LACP bundling | v1 |
-
-## Resources
-
-- [Magic Transit Docs](https://developers.cloudflare.com/magic-transit/)
-- [Magic WAN Docs](https://developers.cloudflare.com/magic-wan/)
-- [Argo Smart Routing](https://developers.cloudflare.com/argo/)
diff --git a/.agents/skills/cloudflare-deploy/references/observability/README.md b/.agents/skills/cloudflare-deploy/references/observability/README.md
deleted file mode 100644
index 58feed6..0000000
--- a/.agents/skills/cloudflare-deploy/references/observability/README.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Cloudflare Observability Skill Reference
-
-**Purpose**: Comprehensive guidance for implementing observability in Cloudflare Workers, covering traces, logs, metrics, and analytics.
-
-**Scope**: Cloudflare Observability features ONLY - Workers Logs, Traces, Analytics Engine, Logpush, Metrics & Analytics, and OpenTelemetry exports.
-
----
-
-## Decision Tree: Which File to Load?
-
-Use this to route to the correct file without loading all content:
-
-```
-├─ "How do I enable/configure X?" → configuration.md
-├─ "What's the API/method/binding for X?" → api.md
-├─ "How do I implement X pattern?" → patterns.md
-│ ├─ Usage tracking/billing → patterns.md
-│ ├─ Error tracking → patterns.md
-│ ├─ Performance monitoring → patterns.md
-│ ├─ Multi-tenant tracking → patterns.md
-│ ├─ Tail Worker filtering → patterns.md
-│ └─ OpenTelemetry export → patterns.md
-└─ "Why isn't X working?" / "Limits?" → gotchas.md
-```
-
-## Reading Order
-
-Load files in this order based on task:
-
-| Task Type | Load Order | Reason |
-|-----------|------------|--------|
-| **Initial setup** | configuration.md → gotchas.md | Setup first, avoid pitfalls |
-| **Implement feature** | patterns.md → api.md → gotchas.md | Pattern → API details → edge cases |
-| **Debug issue** | gotchas.md → configuration.md | Common issues first |
-| **Query data** | api.md → patterns.md | API syntax → query examples |
-
-## Product Overview
-
-### Workers Logs
-- **What:** Console output from Workers (console.log/warn/error)
-- **Access:** Dashboard (Real-time Logs), Logpush, Tail Workers
-- **Cost:** Free (included with all Workers)
-- **Retention:** Real-time only (no historical storage in dashboard)
-
-### Workers Traces
-- **What:** Execution traces with timing, CPU usage, outcome
-- **Access:** Dashboard (Workers Analytics → Traces), Logpush
-- **Cost:** $0.10/1M spans (GA pricing starts March 1, 2026), 10M free/month
-- **Retention:** 14 days included
-
-### Analytics Engine
-- **What:** High-cardinality event storage and SQL queries
-- **Access:** SQL API, Dashboard (Analytics → Analytics Engine)
-- **Cost:** $0.25/1M writes beyond 10M free/month
-- **Retention:** 90 days (configurable up to 1 year)
-
-### Tail Workers
-- **What:** Workers that receive logs/traces from other Workers
-- **Use Cases:** Log filtering, transformation, external export
-- **Cost:** Standard Workers pricing
-
-### Logpush
-- **What:** Stream logs to external storage (S3, R2, Datadog, etc.)
-- **Access:** Dashboard, API
-- **Cost:** Requires Business/Enterprise plan
-
-## Pricing Summary (2026)
-
-| Feature | Free Tier | Cost Beyond Free Tier | Plan Requirement |
-|---------|-----------|----------------------|------------------|
-| Workers Logs | Unlimited | Free | Any |
-| Workers Traces | 10M spans/month | $0.10/1M spans | Paid Workers (GA: March 1, 2026) |
-| Analytics Engine | 10M writes/month | $0.25/1M writes | Paid Workers |
-| Logpush | N/A | Included in plan | Business/Enterprise |
-
-## In This Reference
-
-- **[configuration.md](configuration.md)** - Setup, deployment, configuration (Logs, Traces, Analytics Engine, Tail Workers, Logpush)
-- **[api.md](api.md)** - API endpoints, methods, interfaces (GraphQL, SQL, bindings, types)
-- **[patterns.md](patterns.md)** - Common patterns, use cases, examples (billing, monitoring, error tracking, exports)
-- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations (common errors, performance gotchas, pricing)
-
-## See Also
-
-- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
-- [Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)
-- [Workers Traces Docs](https://developers.cloudflare.com/workers/observability/traces/)
diff --git a/.agents/skills/cloudflare-deploy/references/observability/api.md b/.agents/skills/cloudflare-deploy/references/observability/api.md
deleted file mode 100644
index a0161de..0000000
--- a/.agents/skills/cloudflare-deploy/references/observability/api.md
+++ /dev/null
@@ -1,164 +0,0 @@
-## API Reference
-
-### GraphQL Analytics API
-
-**Endpoint**: `https://api.cloudflare.com/client/v4/graphql`
-
-**Query Workers Metrics**:
-```graphql
-query {
- viewer {
- accounts(filter: { accountTag: $accountId }) {
- workersInvocationsAdaptive(
- limit: 100
- filter: {
- datetime_geq: "2025-01-01T00:00:00Z"
- datetime_leq: "2025-01-31T23:59:59Z"
- scriptName: "my-worker"
- }
- ) {
- sum {
- requests
- errors
- subrequests
- }
- quantiles {
- cpuTimeP50
- cpuTimeP99
- wallTimeP50
- wallTimeP99
- }
- }
- }
- }
-}
-```
-
-### Analytics Engine SQL API
-
-**Endpoint**: `https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql`
-
-**Authentication**: `Authorization: Bearer ` (Account Analytics Read permission)
-
-**Common Queries**:
-
-```sql
--- List all datasets
-SHOW TABLES;
-
--- Time-series aggregation (5-minute buckets)
-SELECT
- intDiv(toUInt32(timestamp), 300) * 300 AS time_bucket,
- blob1 AS endpoint,
- SUM(_sample_interval) AS total_requests,
- AVG(double1) AS avg_response_time_ms
-FROM api_metrics
-WHERE timestamp >= NOW() - INTERVAL '24' HOUR
-GROUP BY time_bucket, endpoint
-ORDER BY time_bucket DESC;
-
--- Top customers by usage
-SELECT
- index1 AS customer_id,
- SUM(_sample_interval * double1) AS total_api_calls,
- AVG(double2) AS avg_response_time_ms
-FROM api_usage
-WHERE timestamp >= NOW() - INTERVAL '7' DAY
-GROUP BY customer_id
-ORDER BY total_api_calls DESC
-LIMIT 100;
-
--- Error rate analysis
-SELECT
- blob1 AS error_type,
- COUNT(*) AS occurrences,
- MAX(timestamp) AS last_seen
-FROM error_tracking
-WHERE timestamp >= NOW() - INTERVAL '1' HOUR
-GROUP BY error_type
-ORDER BY occurrences DESC;
-```
-
-### Console Logging API
-
-**Methods**:
-```typescript
-// Standard methods (all appear in Workers Logs)
-console.log('info message');
-console.info('info message');
-console.warn('warning message');
-console.error('error message');
-console.debug('debug message');
-
-// Structured logging (recommended)
-console.log({
- level: 'info',
- user_id: '123',
- action: 'checkout',
- amount: 99.99,
- currency: 'USD'
-});
-```
-
-**Log Levels**: All console methods produce logs; use structured fields for filtering:
-```typescript
-console.log({
- level: 'error',
- message: 'Payment failed',
- error_code: 'CARD_DECLINED'
-});
-```
-
-### Analytics Engine Binding Types
-
-```typescript
-interface AnalyticsEngineDataset {
- writeDataPoint(event: AnalyticsEngineDataPoint): void;
-}
-
-interface AnalyticsEngineDataPoint {
- // Indexed strings (use for filtering/grouping)
- indexes?: string[];
-
- // Non-indexed strings (metadata, IDs, URLs)
- blobs?: string[];
-
- // Numeric values (counts, durations, amounts)
- doubles?: number[];
-}
-```
-
-**Field Limits**:
-- Max 20 indexes
-- Max 20 blobs
-- Max 20 doubles
-- Max 25 `writeDataPoint` calls per request
-
-### Tail Consumer Event Type
-
-```typescript
-interface TraceItem {
- event: TraceEvent;
- logs: TraceLog[];
- exceptions: TraceException[];
- scriptName?: string;
-}
-
-interface TraceEvent {
- outcome: 'ok' | 'exception' | 'exceededCpu' | 'exceededMemory' | 'unknown';
- cpuTime: number; // microseconds
- wallTime: number; // microseconds
-}
-
-interface TraceLog {
- timestamp: number;
- level: 'log' | 'info' | 'debug' | 'warn' | 'error';
- message: any; // string or structured object
-}
-
-interface TraceException {
- name: string;
- message: string;
- timestamp: number;
-}
-```
\ No newline at end of file
diff --git a/.agents/skills/cloudflare-deploy/references/observability/configuration.md b/.agents/skills/cloudflare-deploy/references/observability/configuration.md
deleted file mode 100644
index 483de4c..0000000
--- a/.agents/skills/cloudflare-deploy/references/observability/configuration.md
+++ /dev/null
@@ -1,169 +0,0 @@
-## Configuration Patterns
-
-### Enable Workers Logs
-
-```jsonc
-{
- "observability": {
- "enabled": true,
- "head_sampling_rate": 1 // 100% sampling (default)
- }
-}
-```
-
-**Best Practice**: Use structured JSON logging for better indexing
-
-```typescript
-// Good - structured logging
-console.log({
- user_id: 123,
- action: "login",
- status: "success",
- duration_ms: 45
-});
-
-// Avoid - unstructured string
-console.log("user_id: 123 logged in successfully in 45ms");
-```
-
-### Enable Workers Traces
-
-```jsonc
-{
- "observability": {
- "traces": {
- "enabled": true,
- "head_sampling_rate": 0.05 // 5% sampling
- }
- }
-}
-```
-
-**Note**: Default sampling is 100%. For high-traffic Workers, use lower sampling (0.01-0.1).
-
-### Configure Analytics Engine
-
-**Bind to Worker**:
-```toml
-# wrangler.toml
-analytics_engine_datasets = [
- { binding = "ANALYTICS", dataset = "api_metrics" }
-]
-```
-
-**Write Data Points**:
-```typescript
-export interface Env {
- ANALYTICS: AnalyticsEngineDataset;
-}
-
-export default {
- async fetch(request: Request, env: Env): Promise {
- // Track metrics
- env.ANALYTICS.writeDataPoint({
- blobs: ['customer_123', 'POST', '/api/v1/users'],
- doubles: [1, 245.5], // request_count, response_time_ms
- indexes: ['customer_123'] // for efficient filtering
- });
-
- return new Response('OK');
- }
-}
-```
-
-### Configure Tail Workers
-
-Tail Workers receive logs/traces from other Workers for filtering, transformation, or export.
-
-**Setup**:
-```toml
-# wrangler.toml
-name = "log-processor"
-main = "src/tail.ts"
-
-[[tail_consumers]]
-service = "my-worker" # Worker to tail
-```
-
-**Tail Worker Example**:
-```typescript
-export default {
- async tail(events: TraceItem[], env: Env, ctx: ExecutionContext) {
- // Filter errors only
- const errors = events.filter(event =>
- event.outcome === 'exception' || event.outcome === 'exceededCpu'
- );
-
- if (errors.length > 0) {
- // Send to external monitoring
- ctx.waitUntil(
- fetch('https://monitoring.example.com/errors', {
- method: 'POST',
- body: JSON.stringify(errors)
- })
- );
- }
- }
-}
-```
-
-### Configure Logpush
-
-Send logs to external storage (S3, R2, GCS, Azure, Datadog, etc.). Requires Business/Enterprise plan.
-
-**Via Dashboard**:
-1. Navigate to Analytics → Logs → Logpush
-2. Select destination type
-3. Provide credentials and bucket/endpoint
-4. Choose dataset (e.g., Workers Trace Events)
-5. Configure filters and fields
-
-**Via API**:
-```bash
-curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/logpush/jobs" \
- -H "Authorization: Bearer " \
- -H "Content-Type: application/json" \
- -d '{
- "name": "workers-logs-to-s3",
- "destination_conf": "s3://my-bucket/logs?region=us-east-1",
- "dataset": "workers_trace_events",
- "enabled": true,
- "frequency": "high",
- "filter": "{\"where\":{\"and\":[{\"key\":\"ScriptName\",\"operator\":\"eq\",\"value\":\"my-worker\"}]}}"
- }'
-```
-
-### Environment-Specific Configuration
-
-**Development** (verbose logs, full sampling):
-```jsonc
-// wrangler.dev.jsonc
-{
- "observability": {
- "enabled": true,
- "head_sampling_rate": 1.0,
- "traces": {
- "enabled": true
- }
- }
-}
-```
-
-**Production** (reduced sampling, structured logs):
-```jsonc
-// wrangler.prod.jsonc
-{
- "observability": {
- "enabled": true,
- "head_sampling_rate": 0.1, // 10% sampling
- "traces": {
- "enabled": true
- }
- }
-}
-```
-
-Deploy with env-specific config:
-```bash
-wrangler deploy --config wrangler.prod.jsonc --env production
-```
\ No newline at end of file
diff --git a/.agents/skills/cloudflare-deploy/references/observability/gotchas.md b/.agents/skills/cloudflare-deploy/references/observability/gotchas.md
deleted file mode 100644
index 42bc738..0000000
--- a/.agents/skills/cloudflare-deploy/references/observability/gotchas.md
+++ /dev/null
@@ -1,115 +0,0 @@
-## Common Errors
-
-### "Logs not appearing"
-
-**Cause:** Observability disabled, Worker not redeployed, no traffic, low sampling rate, or log size exceeds 256 KB
-**Solution:**
-```bash
-# Verify config
-cat wrangler.jsonc | jq '.observability'
-
-# Check deployment
-wrangler deployments list
-
-# Test with curl
-curl https://your-worker.workers.dev
-```
-Ensure `observability.enabled = true`, redeploy Worker, check `head_sampling_rate`, verify traffic
-
-### "Traces not being captured"
-
-**Cause:** Traces not enabled, incorrect sampling rate, Worker not redeployed, or destination unavailable
-**Solution:**
-```jsonc
-// Temporarily set to 100% sampling for debugging
-{
- "observability": {
- "enabled": true,
- "head_sampling_rate": 1.0,
- "traces": {
- "enabled": true
- }
- }
-}
-```
-Ensure `observability.traces.enabled = true`, set `head_sampling_rate` to 1.0 for testing, redeploy, check destination status
-
-## Limits
-
-| Resource/Limit | Value | Notes |
-|----------------|-------|-------|
-| Max log size | 256 KB | Logs exceeding this are truncated |
-| Default sampling rate | 1.0 (100%) | Reduce for high-traffic Workers |
-| Max destinations | Varies by plan | Check dashboard |
-| Trace context propagation | 100 spans max | Deep call chains may lose spans |
-| Analytics Engine write rate | 25 writes/request | Excess writes dropped silently |
-
-## Performance Gotchas
-
-### Spectre Mitigation Timing
-
-**Problem:** `Date.now()` and `performance.now()` have reduced precision (coarsened to 100μs)
-**Cause:** Spectre vulnerability mitigation in V8
-**Solution:** Accept reduced precision or use Workers Traces for accurate timing
-```typescript
-// Date.now() is coarsened - trace spans are accurate
-export default {
- async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
- // For user-facing timing, Date.now() is fine
- const start = Date.now();
- const response = await processRequest(request);
- const duration = Date.now() - start;
-
- // For detailed performance analysis, use Workers Traces instead
- return response;
- }
-}
-```
-
-### Analytics Engine _sample_interval Aggregation
-
-**Problem:** Queries return incorrect totals when not multiplying by `_sample_interval`
-**Cause:** Analytics Engine stores sampled data points, each representing multiple events
-**Solution:** Always multiply counts/sums by `_sample_interval` in aggregations
-```sql
--- WRONG: Undercounts actual events
-SELECT blob1 AS customer_id, COUNT(*) AS total_calls
-FROM api_usage GROUP BY customer_id;
-
--- CORRECT: Accounts for sampling
-SELECT blob1 AS customer_id, SUM(_sample_interval) AS total_calls
-FROM api_usage GROUP BY customer_id;
-```
-
-### Trace Context Propagation Limits
-
-**Problem:** Deep call chains lose trace context after 100 spans
-**Cause:** Cloudflare limits trace depth to prevent performance impact
-**Solution:** Design for flatter architectures or use custom correlation IDs for deep chains
-```typescript
-// For deep call chains, add custom correlation ID
-const correlationId = crypto.randomUUID();
-console.log({ correlationId, event: 'request_start' });
-
-// Pass correlationId through headers to downstream services
-await fetch('https://api.example.com', {
- headers: { 'X-Correlation-ID': correlationId }
-});
-```
-
-## Pricing (2026)
-
-### Workers Traces
-- **GA Pricing (starts March 1, 2026):**
- - $0.10 per 1M trace spans captured
- - Retention: 14 days included
-- **Free tier:** 10M trace spans/month
-- **Note:** Beta usage (before March 1, 2026) is free
-
-### Workers Logs
-- **Included:** Free for all Workers
-- **Logpush:** Requires Business/Enterprise plan
-
-### Analytics Engine
-- **Included:** 10M writes/month on Paid Workers plan
-- **Additional:** $0.25 per 1M writes beyond included quota
diff --git a/.agents/skills/cloudflare-deploy/references/observability/patterns.md b/.agents/skills/cloudflare-deploy/references/observability/patterns.md
deleted file mode 100644
index 9135c68..0000000
--- a/.agents/skills/cloudflare-deploy/references/observability/patterns.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Observability Patterns
-
-## Usage-Based Billing
-
-```typescript
-env.ANALYTICS.writeDataPoint({
- blobs: [customerId, request.url, request.method],
- doubles: [1], // request_count
- indexes: [customerId]
-});
-```
-
-```sql
-SELECT blob1 AS customer_id, SUM(_sample_interval * double1) AS total_calls
-FROM api_usage WHERE timestamp >= DATE_TRUNC('month', NOW())
-GROUP BY customer_id
-```
-
-## Performance Monitoring
-
-```typescript
-const start = Date.now();
-const response = await fetch(url);
-env.ANALYTICS.writeDataPoint({
- blobs: [url, response.status.toString()],
- doubles: [Date.now() - start, response.status]
-});
-```
-
-```sql
-SELECT blob1 AS url, AVG(double1) AS avg_ms, percentile(double1, 0.95) AS p95_ms
-FROM fetch_metrics WHERE timestamp >= NOW() - INTERVAL '1' HOUR
-GROUP BY url
-```
-
-## Error Tracking
-
-```typescript
-env.ANALYTICS.writeDataPoint({
- blobs: [error.name, request.url, request.method],
- doubles: [1],
- indexes: [error.name]
-});
-```
-
-## Multi-Tenant Tracking
-
-```typescript
-env.ANALYTICS.writeDataPoint({
- indexes: [tenantId], // efficient filtering
- blobs: [tenantId, url.pathname, method, status],
- doubles: [1, duration, bytesSize]
-});
-```
-
-## Tail Worker Log Filtering
-
-```typescript
-export default {
- async tail(events, env, ctx) {
- const critical = events.filter(e =>
- e.exceptions.length > 0 || e.event.wallTime > 1000000
- );
- if (critical.length === 0) return;
-
- ctx.waitUntil(
- fetch('https://logging.example.com/ingest', {
- method: 'POST',
- headers: { 'Authorization': `Bearer ${env.API_KEY}` },
- body: JSON.stringify(critical.map(e => ({
- outcome: e.event.outcome,
- cpu_ms: e.event.cpuTime / 1000,
- errors: e.exceptions
- })))
- })
- );
- }
-};
-```
-
-## OpenTelemetry Export
-
-```typescript
-export default {
- async tail(events, env, ctx) {
- const otelSpans = events.map(e => ({
- traceId: generateId(32),
- spanId: generateId(16),
- name: e.scriptName || 'worker.request',
- attributes: [
- { key: 'worker.outcome', value: { stringValue: e.event.outcome } },
- { key: 'worker.cpu_time_us', value: { intValue: String(e.event.cpuTime) } }
- ]
- }));
-
- ctx.waitUntil(
- fetch('https://api.honeycomb.io/v1/traces', {
- method: 'POST',
- headers: { 'X-Honeycomb-Team': env.HONEYCOMB_KEY },
- body: JSON.stringify({ resourceSpans: [{ scopeSpans: [{ spans: otelSpans }] }] })
- })
- );
- }
-};
-```
diff --git a/.agents/skills/cloudflare-deploy/references/pages-functions/README.md b/.agents/skills/cloudflare-deploy/references/pages-functions/README.md
deleted file mode 100644
index deaf461..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages-functions/README.md
+++ /dev/null
@@ -1,98 +0,0 @@
-# Cloudflare Pages Functions
-
-Serverless functions on Cloudflare Pages using Workers runtime. Full-stack dev with file-based routing.
-
-## Quick Navigation
-
-**Need to...**
-| Task | Go to |
-|------|-------|
-| Set up TypeScript types | [configuration.md](./configuration.md) - TypeScript Setup |
-| Configure bindings (KV, D1, R2) | [configuration.md](./configuration.md) - wrangler.jsonc |
-| Access request/env/params | [api.md](./api.md) - EventContext |
-| Add middleware or auth | [patterns.md](./patterns.md) - Middleware, Auth |
-| Background tasks (waitUntil) | [patterns.md](./patterns.md) - Background Tasks |
-| Debug errors or check limits | [gotchas.md](./gotchas.md) - Common Errors, Limits |
-
-## Decision Tree: Is This Pages Functions?
-
-```
-Need serverless backend?
-├─ Yes, for a static site → Pages Functions
-├─ Yes, standalone API → Workers
-└─ Just static hosting → Pages (no functions)
-
-Have existing Worker?
-├─ Complex routing logic → Use _worker.js (Advanced Mode)
-└─ Simple routes → Migrate to /functions (File-Based)
-
-Framework-based?
-├─ Next.js/SvelteKit/Remix → Uses _worker.js automatically
-└─ Vanilla/HTML/React SPA → Use /functions
-```
-
-## File-Based Routing
-
-```
-/functions
- ├── index.js → /
- ├── api.js → /api
- ├── users/
- │ ├── index.js → /users/
- │ ├── [user].js → /users/:user
- │ └── [[catchall]].js → /users/*
- └── _middleware.js → runs on all routes
-```
-
-**Rules:**
-- `index.js` → directory root
-- Trailing slash optional
-- Specific routes precede catch-alls
-- Falls back to static if no match
-
-## Dynamic Routes
-
-**Single segment** `[param]` → string:
-```js
-// /functions/users/[user].js
-export function onRequest(context) {
- return new Response(`Hello ${context.params.user}`);
-}
-// Matches: /users/nevi
-```
-
-**Multi-segment** `[[param]]` → array:
-```js
-// /functions/users/[[catchall]].js
-export function onRequest(context) {
- return new Response(JSON.stringify(context.params.catchall));
-}
-// Matches: /users/nevi/foobar → ["nevi", "foobar"]
-```
-
-## Key Features
-
-- **Method handlers:** `onRequestGet`, `onRequestPost`, etc.
-- **Middleware:** `_middleware.js` for cross-cutting concerns
-- **Bindings:** KV, D1, R2, Durable Objects, Workers AI, Service bindings
-- **TypeScript:** Full type support via `wrangler types` command
-- **Advanced mode:** Use `_worker.js` for custom routing logic
-
-## Reading Order
-
-**New to Pages Functions?** Start here:
-1. [README.md](./README.md) - Overview, routing, decision tree (you are here)
-2. [configuration.md](./configuration.md) - TypeScript setup, wrangler.jsonc, bindings
-3. [api.md](./api.md) - EventContext, handlers, bindings reference
-4. [patterns.md](./patterns.md) - Middleware, auth, CORS, rate limiting, caching
-5. [gotchas.md](./gotchas.md) - Common errors, debugging, limits
-
-**Quick reference lookup:**
-- Bindings table → [api.md](./api.md)
-- Error diagnosis → [gotchas.md](./gotchas.md)
-- TypeScript setup → [configuration.md](./configuration.md)
-
-## See Also
-- [pages](../pages/) - Pages platform overview and static site deployment
-- [workers](../workers/) - Workers runtime API reference
-- [d1](../d1/) - D1 database integration with Pages Functions
diff --git a/.agents/skills/cloudflare-deploy/references/pages-functions/api.md b/.agents/skills/cloudflare-deploy/references/pages-functions/api.md
deleted file mode 100644
index 5263372..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages-functions/api.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# Function API
-
-## EventContext
-
-```typescript
-interface EventContext {
- request: Request; // Incoming request
- functionPath: string; // Request path
- waitUntil(promise: Promise): void; // Background tasks (non-blocking)
- passThroughOnException(): void; // Fallback to static on error
- next(input?: Request | string, init?: RequestInit): Promise;
- env: Env; // Bindings, vars, secrets
- params: Record; // Route params ([user] or [[catchall]])
- data: any; // Middleware shared state
-}
-```
-
-**TypeScript:** See [configuration.md](./configuration.md) for `wrangler types` setup
-
-## Handlers
-
-```typescript
-// Generic (fallback for any method)
-export async function onRequest(ctx: EventContext): Promise {
- return new Response('Any method');
-}
-
-// Method-specific (takes precedence over generic)
-export async function onRequestGet(ctx: EventContext): Promise {
- return Response.json({ message: 'GET' });
-}
-
-export async function onRequestPost(ctx: EventContext): Promise {
- const body = await ctx.request.json();
- return Response.json({ received: body });
-}
-// Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions
-```
-
-## Bindings Reference
-
-| Binding Type | Interface | Config Key | Use Case |
-|--------------|-----------|------------|----------|
-| KV | `KVNamespace` | `kv_namespaces` | Key-value cache, sessions, config |
-| D1 | `D1Database` | `d1_databases` | Relational data, SQL queries |
-| R2 | `R2Bucket` | `r2_buckets` | Large files, user uploads, assets |
-| Durable Objects | `DurableObjectNamespace` | `durable_objects.bindings` | Stateful coordination, websockets |
-| Workers AI | `Ai` | `ai.binding` | LLM inference, embeddings |
-| Vectorize | `VectorizeIndex` | `vectorize` | Vector search, embeddings |
-| Service Binding | `Fetcher` | `services` | Worker-to-worker RPC |
-| Analytics Engine | `AnalyticsEngineDataset` | `analytics_engine_datasets` | Event logging, metrics |
-| Environment Vars | `string` | `vars` | Non-sensitive config |
-
-See [configuration.md](./configuration.md) for wrangler.jsonc examples.
-
-## Bindings
-
-### KV
-
-```typescript
-interface Env { KV: KVNamespace; }
-export const onRequest: PagesFunction = async (ctx) => {
- await ctx.env.KV.put('key', 'value', { expirationTtl: 3600 });
- const val = await ctx.env.KV.get('key', { type: 'json' });
- const keys = await ctx.env.KV.list({ prefix: 'user:' });
- return Response.json({ val });
-};
-```
-
-### D1
-
-```typescript
-interface Env { DB: D1Database; }
-export const onRequest: PagesFunction = async (ctx) => {
- const user = await ctx.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(123).first();
- return Response.json(user);
-};
-```
-
-### R2
-
-```typescript
-interface Env { BUCKET: R2Bucket; }
-export const onRequest: PagesFunction = async (ctx) => {
- const obj = await ctx.env.BUCKET.get('file.txt');
- if (!obj) return new Response('Not found', { status: 404 });
- await ctx.env.BUCKET.put('file.txt', ctx.request.body);
- return new Response(obj.body);
-};
-```
-
-### Durable Objects
-
-```typescript
-interface Env { COUNTER: DurableObjectNamespace; }
-export const onRequest: PagesFunction = async (ctx) => {
- const stub = ctx.env.COUNTER.get(ctx.env.COUNTER.idFromName('global'));
- return stub.fetch(ctx.request);
-};
-```
-
-### Workers AI
-
-```typescript
-interface Env { AI: Ai; }
-export const onRequest: PagesFunction = async (ctx) => {
- const resp = await ctx.env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });
- return Response.json(resp);
-};
-```
-
-### Service Bindings & Env Vars
-
-```typescript
-interface Env { AUTH: Fetcher; API_KEY: string; }
-export const onRequest: PagesFunction = async (ctx) => {
- // Service binding: forward to another Worker
- return ctx.env.AUTH.fetch(ctx.request);
-
- // Environment variable
- return Response.json({ key: ctx.env.API_KEY });
-};
-```
-
-## Advanced Mode (env.ASSETS)
-
-When using `_worker.js`, access static assets via `env.ASSETS.fetch()`:
-
-```typescript
-interface Env { ASSETS: Fetcher; KV: KVNamespace; }
-
-export default {
- async fetch(request: Request, env: Env): Promise {
- const url = new URL(request.url);
- if (url.pathname.startsWith('/api/')) {
- return Response.json({ data: await env.KV.get('key') });
- }
- return env.ASSETS.fetch(request); // Fallback to static
- }
-} satisfies ExportedHandler;
-```
-
-**See also:** [configuration.md](./configuration.md) for TypeScript setup and wrangler.jsonc | [patterns.md](./patterns.md) for middleware and auth patterns
diff --git a/.agents/skills/cloudflare-deploy/references/pages-functions/configuration.md b/.agents/skills/cloudflare-deploy/references/pages-functions/configuration.md
deleted file mode 100644
index 62ba298..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages-functions/configuration.md
+++ /dev/null
@@ -1,122 +0,0 @@
-# Configuration
-
-## TypeScript Setup
-
-**Generate types from wrangler.jsonc** (replaces deprecated `@cloudflare/workers-types`):
-
-```bash
-npx wrangler types
-```
-
-Creates `worker-configuration.d.ts` with typed `Env` interface based on your bindings.
-
-```typescript
-// functions/api.ts
-export const onRequest: PagesFunction = async (ctx) => {
- // ctx.env.KV, ctx.env.DB, etc. are fully typed
- return Response.json({ ok: true });
-};
-```
-
-**Manual types** (if not using wrangler types):
-
-```typescript
-interface Env {
- KV: KVNamespace;
- DB: D1Database;
- API_KEY: string;
-}
-export const onRequest: PagesFunction = async (ctx) => { /* ... */ };
-```
-
-## wrangler.jsonc
-
-```jsonc
-{
- "$schema": "./node_modules/wrangler/config-schema.json",
- "name": "my-pages-app",
- "pages_build_output_dir": "./dist",
- "compatibility_date": "2025-01-01",
- "compatibility_flags": ["nodejs_compat"],
-
- "vars": { "API_URL": "https://api.example.com" },
- "kv_namespaces": [{ "binding": "KV", "id": "abc123" }],
- "d1_databases": [{ "binding": "DB", "database_name": "prod-db", "database_id": "xyz789" }],
- "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }],
- "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter", "script_name": "counter-worker" }] },
- "services": [{ "binding": "AUTH", "service": "auth-worker" }],
- "ai": { "binding": "AI" },
- "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }],
- "analytics_engine_datasets": [{ "binding": "ANALYTICS" }]
-}
-```
-
-## Environment Overrides
-
-Top-level → local dev, `env.preview` → preview, `env.production` → production
-
-```jsonc
-{
- "vars": { "API_URL": "http://localhost:8787" },
- "env": {
- "production": { "vars": { "API_URL": "https://api.example.com" } }
- }
-}
-```
-
-**Note:** If overriding `vars`, `kv_namespaces`, `d1_databases`, etc., ALL must be redefined (non-inheritable)
-
-## Local Secrets (.dev.vars)
-
-**Local dev only** - NOT deployed:
-
-```bash
-# .dev.vars (add to .gitignore)
-SECRET_KEY="my-secret-value"
-```
-
-Accessed via `ctx.env.SECRET_KEY`. Set production secrets:
-```bash
-echo "value" | npx wrangler pages secret put SECRET_KEY --project-name=my-app
-```
-
-## Static Config Files
-
-**_routes.json** - Custom routing:
-```json
-{ "version": 1, "include": ["/api/*"], "exclude": ["/static/*"] }
-```
-
-**_headers** - Static headers:
-```
-/static/*
- Cache-Control: public, max-age=31536000
-```
-
-**_redirects** - Redirects:
-```
-/old /new 301
-```
-
-## Local Dev & Deployment
-
-```bash
-# Dev server
-npx wrangler pages dev ./dist
-
-# With bindings
-npx wrangler pages dev ./dist --kv=KV --d1=DB=db-id --r2=BUCKET
-
-# Durable Objects (2 terminals)
-cd do-worker && npx wrangler dev
-cd pages-project && npx wrangler pages dev ./dist --do COUNTER=Counter@do-worker
-
-# Deploy
-npx wrangler pages deploy ./dist
-npx wrangler pages deploy ./dist --branch preview
-
-# Download config
-npx wrangler pages download config my-project
-```
-
-**See also:** [api.md](./api.md) for binding usage examples
diff --git a/.agents/skills/cloudflare-deploy/references/pages-functions/gotchas.md b/.agents/skills/cloudflare-deploy/references/pages-functions/gotchas.md
deleted file mode 100644
index f63e608..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages-functions/gotchas.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# Gotchas & Debugging
-
-## Error Diagnosis
-
-| Symptom | Likely Cause | Solution |
-|---------|--------------|----------|
-| **Function not invoking** | Wrong `/functions` location, wrong extension, or `_routes.json` excludes path | Check `pages_build_output_dir`, use `.js`/`.ts`, verify `_routes.json` |
-| **`ctx.env.BINDING` undefined** | Binding not configured or name mismatch | Add to `wrangler.jsonc`, verify exact name (case-sensitive), redeploy |
-| **TypeScript errors on `ctx.env`** | Missing type definition | Run `wrangler types` or define `interface Env {}` |
-| **Middleware not running** | Wrong filename/location or missing `ctx.next()` | Name exactly `_middleware.js`, export `onRequest`, call `ctx.next()` |
-| **Secrets missing in production** | `.dev.vars` not deployed | `.dev.vars` is local only - set production secrets via dashboard or `wrangler secret put` |
-| **Type mismatch on binding** | Wrong interface type | See [api.md](./api.md) bindings table for correct types |
-| **"KV key not found" but exists** | Key in wrong namespace or env | Verify namespace binding, check preview vs production env |
-| **Function times out** | Synchronous wait or missing `await` | All I/O must be async/await, use `ctx.waitUntil()` for background tasks |
-
-## Common Errors
-
-### TypeScript type errors
-
-**Problem:** `ctx.env.MY_BINDING` shows type error
-**Cause:** No type definition for `Env`
-**Solution:** Run `npx wrangler types` or manually define:
-```typescript
-interface Env { MY_BINDING: KVNamespace; }
-export const onRequest: PagesFunction = async (ctx) => { /* ... */ };
-```
-
-### Secrets not available in production
-
-**Problem:** `ctx.env.SECRET_KEY` is undefined in production
-**Cause:** `.dev.vars` is local-only, not deployed
-**Solution:** Set production secrets:
-```bash
-echo "value" | npx wrangler pages secret put SECRET_KEY --project-name=my-app
-```
-
-## Debugging
-
-```typescript
-// Console logging
-export async function onRequest(ctx) {
- console.log('Request:', ctx.request.method, ctx.request.url);
- const res = await ctx.next();
- console.log('Status:', res.status);
- return res;
-}
-```
-
-```bash
-# Stream real-time logs
-npx wrangler pages deployment tail
-npx wrangler pages deployment tail --status error
-```
-
-```jsonc
-// Source maps (wrangler.jsonc)
-{ "upload_source_maps": true }
-```
-
-## Limits
-
-| Resource | Free | Paid |
-|----------|------|------|
-| CPU time | 10ms | 50ms |
-| Memory | 128 MB | 128 MB |
-| Script size | 10 MB compressed | 10 MB compressed |
-| Env vars | 5 KB per var, 64 max | 5 KB per var, 64 max |
-| Requests | 100k/day | Unlimited ($0.50/million) |
-
-## Best Practices
-
-**Performance:** Minimize deps (cold start), use KV for cache/D1 for relational/R2 for large files, set `Cache-Control` headers, batch DB ops, handle errors gracefully
-
-**Security:** Never commit secrets (use `.dev.vars` + gitignore), validate input, sanitize before DB, implement auth middleware, set CORS headers, rate limit per-IP
-
-## Migration
-
-**Workers → Pages Functions:**
-- `export default { fetch(req, env) {} }` → `export function onRequest(ctx) { const { request, env } = ctx; }`
-- Use `_worker.js` for complex routing: `env.ASSETS.fetch(request)` for static files
-
-**Other platforms → Pages:**
-- File-based routing: `/functions/api/users.js` → `/api/users`
-- Dynamic routes: `[param]` not `:param`
-- Replace Node.js deps with Workers APIs or add `nodejs_compat` flag
-
-## Resources
-
-- [Official Docs](https://developers.cloudflare.com/pages/functions/)
-- [Workers APIs](https://developers.cloudflare.com/workers/runtime-apis/)
-- [Examples](https://github.com/cloudflare/pages-example-projects)
-- [Discord](https://discord.gg/cloudflaredev)
-
-**See also:** [configuration.md](./configuration.md) for TypeScript setup | [patterns.md](./patterns.md) for middleware/auth | [api.md](./api.md) for bindings
diff --git a/.agents/skills/cloudflare-deploy/references/pages-functions/patterns.md b/.agents/skills/cloudflare-deploy/references/pages-functions/patterns.md
deleted file mode 100644
index 22289e8..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages-functions/patterns.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# Common Patterns
-
-## Background Tasks (waitUntil)
-
-Non-blocking tasks after response sent (analytics, cleanup, webhooks):
-
-```typescript
-export async function onRequest(ctx: EventContext) {
- const res = Response.json({ success: true });
-
- ctx.waitUntil(ctx.env.KV.put('last-visit', new Date().toISOString()));
- ctx.waitUntil(Promise.all([
- ctx.env.ANALYTICS.writeDataPoint({ event: 'view' }),
- fetch('https://webhook.site/...', { method: 'POST' })
- ]));
-
- return res; // Returned immediately
-}
-```
-
-## Middleware & Auth
-
-```typescript
-// functions/_middleware.js (global) or functions/users/_middleware.js (scoped)
-export async function onRequest(ctx) {
- try { return await ctx.next(); }
- catch (err) { return new Response(err.message, { status: 500 }); }
-}
-
-// Chained: export const onRequest = [errorHandler, auth, logger];
-
-// Auth
-async function auth(ctx: EventContext) {
- const token = ctx.request.headers.get('authorization')?.replace('Bearer ', '');
- if (!token) return new Response('Unauthorized', { status: 401 });
- const session = await ctx.env.KV.get(`session:${token}`);
- if (!session) return new Response('Invalid', { status: 401 });
- ctx.data.user = JSON.parse(session);
- return ctx.next();
-}
-```
-
-## CORS & Rate Limiting
-
-```typescript
-// CORS middleware
-const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST' };
-export async function onRequestOptions() { return new Response(null, { headers: cors }); }
-export async function onRequest(ctx) {
- const res = await ctx.next();
- Object.entries(cors).forEach(([k, v]) => res.headers.set(k, v));
- return res;
-}
-
-// Rate limiting (KV-based)
-async function rateLimit(ctx: EventContext) {
- const ip = ctx.request.headers.get('CF-Connecting-IP') || 'unknown';
- const count = parseInt(await ctx.env.KV.get(`rate:${ip}`) || '0');
- if (count >= 100) return new Response('Rate limited', { status: 429 });
- await ctx.env.KV.put(`rate:${ip}`, (count + 1).toString(), { expirationTtl: 3600 });
- return ctx.next();
-}
-```
-
-## Forms, Caching, Redirects
-
-```typescript
-// JSON & file upload
-export async function onRequestPost(ctx) {
- const ct = ctx.request.headers.get('content-type') || '';
- if (ct.includes('application/json')) return Response.json(await ctx.request.json());
- if (ct.includes('multipart/form-data')) {
- const file = (await ctx.request.formData()).get('file') as File;
- await ctx.env.BUCKET.put(file.name, file.stream());
- return Response.json({ uploaded: file.name });
- }
-}
-
-// Cache API
-export async function onRequest(ctx) {
- let res = await caches.default.match(ctx.request);
- if (!res) {
- res = new Response('Data');
- res.headers.set('Cache-Control', 'public, max-age=3600');
- ctx.waitUntil(caches.default.put(ctx.request, res.clone()));
- }
- return res;
-}
-
-// Redirects
-export async function onRequest(ctx) {
- if (new URL(ctx.request.url).pathname === '/old') {
- return Response.redirect(new URL('/new', ctx.request.url), 301);
- }
- return ctx.next();
-}
-```
-
-## Testing
-
-**Unit tests** (Vitest + cloudflare:test):
-```typescript
-import { env } from 'cloudflare:test';
-import { it, expect } from 'vitest';
-import { onRequest } from '../functions/api';
-
-it('returns JSON', async () => {
- const req = new Request('http://localhost/api');
- const ctx = { request: req, env, params: {}, data: {} } as EventContext;
- const res = await onRequest(ctx);
- expect(res.status).toBe(200);
-});
-```
-
-**Integration:** `wrangler pages dev` + Playwright/Cypress
-
-## Advanced Mode (_worker.js)
-
-Use `_worker.js` for complex routing (replaces `/functions`):
-
-```typescript
-interface Env { ASSETS: Fetcher; KV: KVNamespace; }
-
-export default {
- async fetch(request: Request, env: Env): Promise {
- const url = new URL(request.url);
- if (url.pathname.startsWith('/api/')) {
- return Response.json({ data: await env.KV.get('key') });
- }
- return env.ASSETS.fetch(request); // Static files
- }
-} satisfies ExportedHandler;
-```
-
-**When:** Existing Worker, framework-generated (Next.js/SvelteKit), custom routing logic
-
-**See also:** [api.md](./api.md) for `env.ASSETS.fetch()` | [gotchas.md](./gotchas.md) for debugging
diff --git a/.agents/skills/cloudflare-deploy/references/pages/README.md b/.agents/skills/cloudflare-deploy/references/pages/README.md
deleted file mode 100644
index bf0546f..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages/README.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Cloudflare Pages
-
-JAMstack platform for full-stack apps on Cloudflare's global network.
-
-## Key Features
-
-- **Git-based deploys**: Auto-deploy from GitHub/GitLab
-- **Preview deployments**: Unique URL per branch/PR
-- **Pages Functions**: File-based serverless routing (Workers runtime)
-- **Static + dynamic**: Smart asset caching + edge compute
-- **Smart Placement**: Automatic function optimization based on traffic patterns
-- **Framework optimized**: SvelteKit, Astro, Nuxt, Qwik, Solid Start
-
-## Deployment Methods
-
-### 1. Git Integration (Production)
-Dashboard → Workers & Pages → Create → Connect to Git → Configure build
-
-### 2. Direct Upload
-```bash
-npx wrangler pages deploy ./dist --project-name=my-project
-npx wrangler pages deploy ./dist --project-name=my-project --branch=staging
-```
-
-### 3. C3 CLI
-```bash
-npm create cloudflare@latest my-app
-# Select framework → auto-setup + deploy
-```
-
-## vs Workers
-
-- **Pages**: Static sites, JAMstack, frameworks, git workflow, file-based routing
-- **Workers**: Pure APIs, complex routing, WebSockets, scheduled tasks, email handlers
-- **Combine**: Pages Functions use Workers runtime, can bind to Workers
-
-## Quick Start
-
-```bash
-# Create
-npm create cloudflare@latest
-
-# Local dev
-npx wrangler pages dev ./dist
-
-# Deploy
-npx wrangler pages deploy ./dist --project-name=my-project
-
-# Types
-npx wrangler types --path='./functions/types.d.ts'
-
-# Secrets
-echo "value" | npx wrangler pages secret put KEY --project-name=my-project
-
-# Logs
-npx wrangler pages deployment tail --project-name=my-project
-```
-
-## Resources
-
-- [Pages Docs](https://developers.cloudflare.com/pages/)
-- [Functions API](https://developers.cloudflare.com/pages/functions/api-reference/)
-- [Framework Guides](https://developers.cloudflare.com/pages/framework-guides/)
-- [Discord #functions](https://discord.com/channels/595317990191398933/910978223968518144)
-
-## Reading Order
-
-**New to Pages?** Start here:
-1. README.md (you are here) - Overview & quick start
-2. [configuration.md](./configuration.md) - Project setup, wrangler.jsonc, bindings
-3. [api.md](./api.md) - Functions API, routing, context
-4. [patterns.md](./patterns.md) - Common implementations
-5. [gotchas.md](./gotchas.md) - Troubleshooting & pitfalls
-
-**Quick reference?** Jump to relevant file above.
-
-## In This Reference
-
-- [configuration.md](./configuration.md) - wrangler.jsonc, build, env vars, Smart Placement
-- [api.md](./api.md) - Functions API, bindings, context, advanced mode
-- [patterns.md](./patterns.md) - Full-stack patterns, framework integration
-- [gotchas.md](./gotchas.md) - Build issues, limits, debugging, framework warnings
-
-## See Also
-
-- [pages-functions](../pages-functions/) - File-based routing, middleware
-- [d1](../d1/) - SQL database for Pages Functions
-- [kv](../kv/) - Key-value storage for caching/state
diff --git a/.agents/skills/cloudflare-deploy/references/pages/api.md b/.agents/skills/cloudflare-deploy/references/pages/api.md
deleted file mode 100644
index a719585..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages/api.md
+++ /dev/null
@@ -1,204 +0,0 @@
-# Functions API
-
-## File-Based Routing
-
-```
-/functions/index.ts → example.com/
-/functions/api/users.ts → example.com/api/users
-/functions/api/users/[id].ts → example.com/api/users/:id
-/functions/api/users/[[path]].ts → example.com/api/users/* (catchall)
-/functions/_middleware.ts → Runs before all routes
-```
-
-**Rules**: `[param]` = single segment, `[[param]]` = multi-segment catchall, more specific wins.
-
-## Request Handlers
-
-```typescript
-import type { PagesFunction } from '@cloudflare/workers-types';
-
-interface Env {
- DB: D1Database;
- KV: KVNamespace;
-}
-
-// All methods
-export const onRequest: PagesFunction = async (context) => {
- return new Response('All methods');
-};
-
-// Method-specific
-export const onRequestGet: PagesFunction = async (context) => {
- const { request, env, params, data } = context;
-
- const user = await env.DB.prepare(
- 'SELECT * FROM users WHERE id = ?'
- ).bind(params.id).first();
-
- return Response.json(user);
-};
-
-export const onRequestPost: PagesFunction = async (context) => {
- const body = await context.request.json();
- return Response.json({ success: true });
-};
-
-// Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions
-```
-
-## Context Object
-
-```typescript
-interface EventContext {
- request: Request; // HTTP request
- env: Env; // Bindings (KV, D1, R2, etc.)
- params: Params; // Route parameters
- data: Data; // Middleware-shared data
- waitUntil: (promise: Promise) => void; // Background tasks
- next: () => Promise; // Next handler
- passThroughOnException: () => void; // Error fallback (not in advanced mode)
-}
-```
-
-## Dynamic Routes
-
-```typescript
-// Single segment: functions/users/[id].ts
-export const onRequestGet: PagesFunction = async ({ params }) => {
- // /users/123 → params.id = "123"
- return Response.json({ userId: params.id });
-};
-
-// Multi-segment: functions/files/[[path]].ts
-export const onRequestGet: PagesFunction = async ({ params }) => {
- // /files/docs/api/v1.md → params.path = ["docs", "api", "v1.md"]
- const filePath = (params.path as string[]).join('/');
- return new Response(filePath);
-};
-```
-
-## Middleware
-
-```typescript
-// functions/_middleware.ts
-// Single
-export const onRequest: PagesFunction = async (context) => {
- const response = await context.next();
- response.headers.set('X-Custom-Header', 'value');
- return response;
-};
-
-// Chained (runs in order)
-const errorHandler: PagesFunction = async (context) => {
- try {
- return await context.next();
- } catch (err) {
- return new Response(err.message, { status: 500 });
- }
-};
-
-const auth: PagesFunction = async (context) => {
- const token = context.request.headers.get('Authorization');
- if (!token) return new Response('Unauthorized', { status: 401 });
- context.data.userId = await verifyToken(token);
- return context.next();
-};
-
-export const onRequest = [errorHandler, auth];
-```
-
-**Scope**: `functions/_middleware.ts` → all; `functions/api/_middleware.ts` → `/api/*` only
-
-## Bindings Usage
-
-```typescript
-export const onRequestGet: PagesFunction = async ({ env }) => {
- // KV
- const cached = await env.KV.get('key', 'json');
- await env.KV.put('key', JSON.stringify({data: 'value'}), {expirationTtl: 3600});
-
- // D1
- const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
-
- // R2, Queue, AI - see respective reference docs
-
- return Response.json({success: true});
-};
-```
-
-## Advanced Mode
-
-Full Workers API, bypasses file-based routing:
-
-```javascript
-// functions/_worker.js
-export default {
- async fetch(request, env, ctx) {
- const url = new URL(request.url);
-
- // Custom routing
- if (url.pathname.startsWith('/api/')) {
- return new Response('API response');
- }
-
- // REQUIRED: Serve static assets
- return env.ASSETS.fetch(request);
- }
-};
-```
-
-**When to use**: WebSockets, complex routing, scheduled handlers, email handlers.
-
-## Smart Placement
-
-Automatically optimizes function execution location based on traffic patterns.
-
-**Configuration** (in wrangler.jsonc):
-```jsonc
-{
- "placement": {
- "mode": "smart" // Enables optimization (default: off)
- }
-}
-```
-
-**How it works**: Analyzes traffic patterns over time and places functions closer to users or data sources (e.g., D1 databases). Requires no code changes.
-
-**Trade-offs**: Initial requests may see slightly higher latency during learning period (hours-days). Performance improves as system optimizes.
-
-**When to use**: Global apps with centralized databases or geographically concentrated traffic sources.
-
-## getRequestContext (Framework SSR)
-
-Access bindings in framework code:
-
-```typescript
-// SvelteKit
-import type { RequestEvent } from '@sveltejs/kit';
-export async function load({ platform }: RequestEvent) {
- const data = await platform.env.DB.prepare('SELECT * FROM users').all();
- return { users: data.results };
-}
-
-// Astro
-const { DB } = Astro.locals.runtime.env;
-const data = await DB.prepare('SELECT * FROM users').all();
-
-// Solid Start (server function)
-import { getRequestEvent } from 'solid-js/web';
-const event = getRequestEvent();
-const data = await event.locals.runtime.env.DB.prepare('SELECT * FROM users').all();
-```
-
-**✅ Supported adapters** (2026):
-- **SvelteKit**: `@sveltejs/adapter-cloudflare`
-- **Astro**: Built-in Cloudflare adapter
-- **Nuxt**: Set `nitro.preset: 'cloudflare-pages'` in `nuxt.config.ts`
-- **Qwik**: Built-in Cloudflare adapter
-- **Solid Start**: `@solidjs/start-cloudflare-pages`
-
-**❌ Deprecated/Unsupported**:
-- **Next.js**: Official adapter (`@cloudflare/next-on-pages`) deprecated. Use Vercel or self-host on Workers.
-- **Remix**: Official adapter (`@remix-run/cloudflare-pages`) deprecated. Migrate to supported frameworks.
-
-See [gotchas.md](./gotchas.md#framework-specific) for migration guidance.
diff --git a/.agents/skills/cloudflare-deploy/references/pages/configuration.md b/.agents/skills/cloudflare-deploy/references/pages/configuration.md
deleted file mode 100644
index 30ada89..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages/configuration.md
+++ /dev/null
@@ -1,201 +0,0 @@
-# Configuration
-
-## wrangler.jsonc
-
-```jsonc
-{
- "name": "my-pages-project",
- "pages_build_output_dir": "./dist",
- "compatibility_date": "2026-01-01", // Use current date for new projects
- "compatibility_flags": ["nodejs_compat"],
- "placement": {
- "mode": "smart" // Optional: Enable Smart Placement
- },
- "kv_namespaces": [{"binding": "KV", "id": "abcd1234..."}],
- "d1_databases": [{"binding": "DB", "database_id": "xxxx-xxxx", "database_name": "production-db"}],
- "r2_buckets": [{"binding": "BUCKET", "bucket_name": "my-bucket"}],
- "durable_objects": {"bindings": [{"name": "COUNTER", "class_name": "Counter", "script_name": "counter-worker"}]},
- "services": [{"binding": "API", "service": "api-worker"}],
- "queues": {"producers": [{"binding": "QUEUE", "queue": "my-queue"}]},
- "vectorize": [{"binding": "VECTORIZE", "index_name": "my-index"}],
- "ai": {"binding": "AI"},
- "analytics_engine_datasets": [{"binding": "ANALYTICS"}],
- "vars": {"API_URL": "https://api.example.com", "ENVIRONMENT": "production"},
- "env": {
- "preview": {
- "vars": {"API_URL": "https://staging-api.example.com"},
- "kv_namespaces": [{"binding": "KV", "id": "preview-namespace-id"}]
- }
- }
-}
-```
-
-## Build Config
-
-**Git deployment**: Dashboard → Project → Settings → Build settings
-Set build command, output dir, env vars. Framework auto-detection configures automatically.
-
-## Environment Variables
-
-### Local (.dev.vars)
-```bash
-# .dev.vars (never commit)
-SECRET_KEY="local-secret-key"
-API_TOKEN="dev-token-123"
-```
-
-### Production
-```bash
-echo "secret-value" | npx wrangler pages secret put SECRET_KEY --project-name=my-project
-npx wrangler pages secret list --project-name=my-project
-npx wrangler pages secret delete SECRET_KEY --project-name=my-project
-```
-
-Access: `env.SECRET_KEY`
-
-## Static Config Files
-
-### _redirects
-Place in build output (e.g., `dist/_redirects`):
-
-```txt
-/old-page /new-page 301 # 301 redirect
-/blog/* /news/:splat 301 # Splat wildcard
-/users/:id /members/:id 301 # Placeholders
-/api/* /api-v2/:splat 200 # Proxy (no redirect)
-```
-
-**Limits**: 2,100 total (2,000 static + 100 dynamic), 1,000 char/line
-**Note**: Functions take precedence
-
-### _headers
-```txt
-/secure/*
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
-
-/api/*
- Access-Control-Allow-Origin: *
-
-/static/*
- Cache-Control: public, max-age=31536000, immutable
-```
-
-**Limits**: 100 rules, 2,000 char/line
-**Note**: Only static assets; Functions set headers in Response
-
-### _routes.json
-Controls which requests invoke Functions (auto-generated for most frameworks):
-
-```json
-{
- "version": 1,
- "include": ["/*"],
- "exclude": ["/build/*", "/static/*", "/assets/*", "/*.{ico,png,jpg,css,js}"]
-}
-```
-
-**Purpose**: Functions are metered; static requests are free. `exclude` takes precedence. Max 100 rules, 100 char/rule.
-
-## TypeScript
-
-```bash
-npx wrangler types --path='./functions/types.d.ts'
-```
-
-Point `types` in `functions/tsconfig.json` to generated file.
-
-## Smart Placement
-
-Automatically optimizes function execution location based on request patterns.
-
-```jsonc
-{
- "placement": {
- "mode": "smart" // Enable optimization (default: off)
- }
-}
-```
-
-**How it works**: System analyzes traffic over hours/days and places function execution closer to:
-- User clusters (e.g., regional traffic)
-- Data sources (e.g., D1 database primary location)
-
-**Benefits**:
-- Lower latency for read-heavy apps with centralized databases
-- Better performance for apps with regional traffic patterns
-
-**Trade-offs**:
-- Initial learning period: First requests may be slower while system optimizes
-- Optimization time: Performance improves over 24-48 hours
-
-**When to enable**: Global apps with D1/Durable Objects in specific regions, or apps with concentrated geographic traffic.
-
-**When to skip**: Evenly distributed global traffic with no data locality constraints.
-
-## Remote Bindings (Local Dev)
-
-Connect local dev server to production bindings instead of local mocks:
-
-```bash
-# All bindings remote
-npx wrangler pages dev ./dist --remote
-
-# Specific bindings remote (others local)
-npx wrangler pages dev ./dist --remote --kv=KV --d1=DB
-```
-
-**Use cases**:
-- Test against production data (read-only operations)
-- Debug binding-specific behavior
-- Validate changes before deployment
-
-**⚠️ Warning**:
-- Writes affect **real production data**
-- Use only for read-heavy debugging or with non-production accounts
-- Consider creating separate preview environments instead
-
-**Requirements**: Must be logged in (`npx wrangler login`) with access to bindings.
-
-## Local Dev
-
-```bash
-# Basic
-npx wrangler pages dev ./dist
-
-# With bindings
-npx wrangler pages dev ./dist --kv KV --d1 DB=local-db-id
-
-# Remote bindings (production data)
-npx wrangler pages dev ./dist --remote
-
-# Persistence
-npx wrangler pages dev ./dist --persist-to=./.wrangler/state/v3
-
-# Proxy mode (SSR frameworks)
-npx wrangler pages dev -- npm run dev
-```
-
-## Limits (as of Jan 2026)
-
-| Resource | Free | Paid |
-|----------|------|------|
-| **Functions Requests** | 100k/day | Unlimited (metered) |
-| **Function CPU Time** | 10ms/req | 30ms/req (Workers Paid) |
-| **Function Memory** | 128MB | 128MB |
-| **Script Size** | 1MB compressed | 10MB compressed |
-| **Deployments** | 500/month | 5,000/month |
-| **Files per Deploy** | 20,000 | 20,000 |
-| **File Size** | 25MB | 25MB |
-| **Build Time** | 20min | 20min |
-| **Redirects** | 2,100 (2k static + 100 dynamic) | Same |
-| **Header Rules** | 100 | 100 |
-| **Route Rules** | 100 | 100 |
-| **Subrequests** | 50/request | 1,000/request (Workers Paid) |
-
-**Notes**:
-- Functions use Workers runtime; Workers Paid plan increases limits
-- Free plan sufficient for most projects
-- Static requests always free (not counted toward limits)
-
-[Full limits](https://developers.cloudflare.com/pages/platform/limits/)
diff --git a/.agents/skills/cloudflare-deploy/references/pages/gotchas.md b/.agents/skills/cloudflare-deploy/references/pages/gotchas.md
deleted file mode 100644
index 943c2d3..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages/gotchas.md
+++ /dev/null
@@ -1,203 +0,0 @@
-# Gotchas
-
-## Functions Not Running
-
-**Problem**: Function endpoints return 404 or don't execute
-**Causes**: `_routes.json` excludes path; wrong file extension (`.jsx`/`.tsx`); Functions dir not at output root
-**Solution**: Check `_routes.json`, rename to `.ts`/`.js`, verify build output structure
-
-## 404 on Static Assets
-
-**Problem**: Static files not serving
-**Causes**: Build output dir misconfigured; Functions catching requests; Advanced mode missing `env.ASSETS.fetch()`
-**Solution**: Verify output dir, add exclusions to `_routes.json`, call `env.ASSETS.fetch()` in `_worker.js`
-
-## Bindings Not Working
-
-**Problem**: `env.BINDING` undefined or errors
-**Causes**: wrangler.jsonc syntax error; wrong binding IDs; missing `.dev.vars`; out-of-sync types
-**Solution**: Validate config, verify IDs, create `.dev.vars`, run `npx wrangler types`
-
-## Build Failures
-
-**Problem**: Deployment fails during build
-**Causes**: Wrong build command/output dir; Node version incompatibility; missing env vars; 20min timeout; OOM
-**Solution**: Check Dashboard → Deployments → Build log; verify settings; add `.nvmrc`; optimize build
-
-## Middleware Not Running
-
-**Problem**: Middleware doesn't execute
-**Causes**: Wrong filename (not `_middleware.ts`); missing `onRequest` export; didn't call `next()`
-**Solution**: Rename file with underscore prefix; export handler; call `next()` or return Response
-
-## Headers/Redirects Not Working
-
-**Problem**: `_headers` or `_redirects` not applying
-**Causes**: Only work for static assets; Functions override; syntax errors; exceeded limits
-**Solution**: Set headers in Response object for Functions; verify syntax; check limits (100 headers, 2,100 redirects)
-
-## TypeScript Errors
-
-**Problem**: Type errors in Functions code
-**Causes**: Types not generated; Env interface doesn't match wrangler.jsonc
-**Solution**: Run `npx wrangler types --path='./functions/types.d.ts'`; update Env interface
-
-## Local Dev Issues
-
-**Problem**: Dev server errors or bindings don't work
-**Causes**: Port conflict; bindings not passed; local vs HTTPS differences
-**Solution**: Use `--port=3000`; pass bindings via CLI or wrangler.jsonc; account for HTTP/HTTPS differences
-
-## Performance Issues
-
-**Problem**: Slow responses or CPU limit errors
-**Causes**: Functions invoked for static assets; cold starts; 10ms CPU limit; large bundle
-**Solution**: Exclude static via `_routes.json`; optimize hot paths; keep bundle < 1MB
-
-## Framework-Specific
-
-### ⚠️ Deprecated Frameworks
-
-**Next.js**: Official adapter (`@cloudflare/next-on-pages`) **deprecated** and unmaintained.
-- **Problem**: No updates since 2024; incompatible with Next.js 15+; missing App Router features
-- **Cause**: Cloudflare discontinued official support; community fork exists but limited
-- **Solutions**:
- 1. **Recommended**: Use Vercel (official Next.js host)
- 2. **Advanced**: Self-host on Workers using custom adapter (complex, unsupported)
- 3. **Migration**: Switch to SvelteKit/Nuxt (similar DX, full Pages support)
-
-**Remix**: Official adapter (`@remix-run/cloudflare-pages`) **deprecated**.
-- **Problem**: No maintenance from Remix team; compatibility issues with Remix v2+
-- **Cause**: Remix team deprecated all framework adapters
-- **Solutions**:
- 1. **Recommended**: Migrate to SvelteKit (similar file-based routing, better DX)
- 2. **Alternative**: Use Astro (static-first with optional SSR)
- 3. **Workaround**: Continue using deprecated adapter (no future support)
-
-### ✅ Supported Frameworks
-
-**SvelteKit**:
-- Use `@sveltejs/adapter-cloudflare`
-- Access bindings via `platform.env` in server load functions
-- Set `platform: 'cloudflare'` in `svelte.config.js`
-
-**Astro**:
-- Built-in Cloudflare adapter
-- Access bindings via `Astro.locals.runtime.env`
-
-**Nuxt**:
-- Set `nitro.preset: 'cloudflare-pages'` in `nuxt.config.ts`
-- Access bindings via `event.context.cloudflare.env`
-
-**Qwik, Solid Start**:
-- Built-in or official Cloudflare adapters available
-- Check respective framework docs for binding access
-
-## Debugging
-
-```typescript
-// Log request details
-console.log('Request:', { method: request.method, url: request.url });
-console.log('Env:', Object.keys(env));
-console.log('Params:', params);
-```
-
-**View logs**: `npx wrangler pages deployment tail --project-name=my-project`
-
-## Smart Placement Issues
-
-### Increased Cold Start Latency
-
-**Problem**: First requests slower after enabling Smart Placement
-**Cause**: Initial optimization period while system learns traffic patterns
-**Solution**: Expected behavior during first 24-48 hours; monitor latency trends over time
-
-### Inconsistent Response Times
-
-**Problem**: Latency varies significantly across requests during initial deployment
-**Cause**: Smart Placement testing different execution locations to find optimal placement
-**Solution**: Normal during learning phase; stabilizes after traffic patterns emerge (1-2 days)
-
-### No Performance Improvement
-
-**Problem**: Smart Placement enabled but no latency reduction observed
-**Cause**: Traffic evenly distributed globally, or no data locality constraints
-**Solution**: Smart Placement most effective with centralized data (D1/DO) or regional traffic; disable if no benefit
-
-## Remote Bindings Issues
-
-### Accidentally Modified Production Data
-
-**Problem**: Local dev with `--remote` altered production database/KV
-**Cause**: Remote bindings connect directly to production resources; writes are real
-**Solution**:
-- Use `--remote` only for read-heavy debugging
-- Create separate preview environments for testing
-- Never use `--remote` for write operations during development
-
-### Remote Binding Auth Errors
-
-**Problem**: `npx wrangler pages dev --remote` fails with "Unauthorized" or auth error
-**Cause**: Not logged in, session expired, or insufficient account permissions
-**Solution**:
-1. Run `npx wrangler login` to re-authenticate
-2. Verify account has access to project and bindings
-3. Check binding IDs match production configuration
-
-### Slow Local Dev with Remote Bindings
-
-**Problem**: Local dev server slow when using `--remote`
-**Cause**: Every request makes network calls to production bindings
-**Solution**: Use local bindings for development; reserve `--remote` for final validation
-
-## Common Errors
-
-### "Module not found"
-**Cause**: Dependencies not bundled or build output incorrect
-**Solution**: Check build output directory, ensure dependencies bundled
-
-### "Binding not found"
-**Cause**: Binding not configured or types out of sync
-**Solution**: Verify wrangler.jsonc, run `npx wrangler types`
-
-### "Request exceeded CPU limit"
-**Cause**: Code execution too slow or heavy compute
-**Solution**: Optimize hot paths, upgrade to Workers Paid
-
-### "Script too large"
-**Cause**: Bundle size exceeds limit
-**Solution**: Tree-shake, use dynamic imports, code-split
-
-### "Too many subrequests"
-**Cause**: Exceeded 50 subrequest limit
-**Solution**: Batch or reduce fetch calls
-
-### "KV key not found"
-**Cause**: Key doesn't exist or wrong namespace
-**Solution**: Check namespace matches environment
-
-### "D1 error"
-**Cause**: Wrong database_id or missing migrations
-**Solution**: Verify config, run `wrangler d1 migrations list`
-
-## Limits Reference (Jan 2026)
-
-| Resource | Free | Paid |
-|----------|------|------|
-| Functions Requests | 100k/day | Unlimited |
-| CPU Time | 10ms/req | 30ms/req |
-| Memory | 128MB | 128MB |
-| Script Size | 1MB | 10MB |
-| Subrequests | 50/req | 1,000/req |
-| Deployments | 500/month | 5,000/month |
-
-**Tip**: Hitting CPU limit? Optimize hot paths or upgrade to Workers Paid plan.
-
-[Full limits](https://developers.cloudflare.com/pages/platform/limits/)
-
-## Getting Help
-
-1. Check [Pages Docs](https://developers.cloudflare.com/pages/)
-2. Search [Discord #functions](https://discord.com/channels/595317990191398933/910978223968518144)
-3. Review [Workers Examples](https://developers.cloudflare.com/workers/examples/)
-4. Check framework-specific docs/adapters
diff --git a/.agents/skills/cloudflare-deploy/references/pages/patterns.md b/.agents/skills/cloudflare-deploy/references/pages/patterns.md
deleted file mode 100644
index 883c4da..0000000
--- a/.agents/skills/cloudflare-deploy/references/pages/patterns.md
+++ /dev/null
@@ -1,204 +0,0 @@
-# Patterns
-
-## API Routes
-
-```typescript
-// functions/api/todos/[id].ts
-export const onRequestGet: PagesFunction