← All Skills

snappy-gateway

v1.0.0
10 files, 105.5 KB ~5,049 words · 21 min read Updated 2026-09-09

snappy-gateway skill

42 of 47 checks pass
What it can do
get nameread
listread
card nameread
publish name? tier?post
pull namewrite-reversible
What does not pass yet
reached bysnappy-skill
Architecture 2 endpoints
🔑 Bearer token auth
api.cloudflare.com2 endpoints
PUT/client/v4/accounts/91e4e6d01964d00df36b85bd7e1549eb/workers/domains
GET/client/v4/accounts/91e4e6d01964d00df36b85bd7e1549eb/workers/domains
$ npx snappy-skills install snappy-gateway
zip ↓
Documents
AGENTS.md

snappy-gateway -- Agent Loader#

You manage the skills.snappy.ai Cloudflare Worker -- a KV-backed gateway that distributes Claude Code skills with tiered access control. This file is the operational contract. Everything load-bearing lives here.

API module#

typescriptimport { listSkills, getSkill, publishSkill } from "../snappy-gateway/api.ts";

Or CLI:

bashnpx tsx ~/.claude/skills/snappy-gateway/api.ts list                  # the catalogue, with each card's check line
npx tsx ~/.claude/skills/snappy-gateway/api.ts get snappy-slack      # a published skill's SKILL.md
npx tsx ~/.claude/skills/snappy-gateway/api.ts card snappy-slack     # build the listing card, publish nothing
npx tsx ~/.claude/skills/snappy-gateway/api.ts publish snappy-slack  # one skill, with its card
npx tsx ~/.claude/skills/snappy-gateway/api.ts publish --base-set    # every base skill; the base-set gate runs first
npx tsx ~/.claude/skills/snappy-gateway/api.ts pull snappy-slack     # Add it, into the person's skills root

THE CARD IS THE LISTING. card.ts parses the skill rather than describing it:

purpose, verbs with their first call, faces, agents and cross-links from the one

AGENTS.md reader; pass/fail and the failing sentences from `snappy-tool-design

lint`; "covers 9 of 174 operations of Slack, checked <date>" from the committed

vendor corpus. Nothing on a card is a claim a person typed.

THE BASE SET IS THE GATE'S ANSWER. --base-set publishes every skill except the

ones scripts/gates/base-set.mjs keeps private, read through its own

personal-tier.mjs. That gate runs BEFORE any upload and a red gate publishes

nothing.

API functions#

Function Purpose
listSkills() List all skills from the gateway catalog (uses master key if available)
getSkill(name) Fetch a specific skill's SKILL.md content from the gateway
publishSkill(skillName, tier?) Publish a local skill directory to gateway KV and rebuild the cross-skill graph

Operating principle#

The gateway is a distribution channel, not an authoring tool. It serves skill files from KV, gates access by tier, and provides an operator console for AI provider credentials. All skill content is authored locally in ~/.claude/skills/ and pushed to KV via the publish script. Never edit skill content through the gateway.


Architecture#

Client Request → skills.snappy.ai (Cloudflare Worker)
  ↓
  index.ts (router) → dispatches by URL path to views or admin handlers
  ↓
  auth.ts → authenticate(request, env) → KeyRecord { tier, name, skills?, expires? }
  ↓
  KV Namespaces:
    SKILLS_STORE  -- skill files + index.json + _graph.json + _logo/*.svg
    API_KEYS      -- access tokens → KeyRecord JSON
    SETTINGS_STORE -- per-user AI provider credentials (keyed by auth token)
  ↓
  Env binding: MASTER_KEY (secret, never in source)

Router pattern (index.ts)#

index.ts is a single fetch() handler. It matches url.pathname top-down:

Path pattern Handler Auth
/login, /logout handleLogin, handleLogout none
/console serveConsole (views/console.ts) session required
/lint serveLintDashboard (views/lint.ts) session required
/settings, /settings/{provider} inline handleSettings session required
/ai/audit handleAiAudit (ai.ts) session required
/admin/api-liveness/* handleApiLivenessAdmin master key (Bearer)
/admin/lint/* handleLintAdmin master key (Bearer or cookie)
/admin/keys, /admin/keys/*, /admin/graph/rebuild, /admin/pid/status handleAdmin master key (Bearer)
/api-liveness.json, /api-refs.json inline read from KV none (public read)
/logo/{dark,light}.svg KV lookup _logo/{theme}.svg none
/install, /install.sh KV lookup _install.sh none
/download/{name}.zip serveZip tier-gated
/skills/{name} serveSkillDetail (views/skill-detail.ts) tier-gated
/.well-known/skills/index.json serveIndex (catalog.ts) tier-filtered
/.well-known/skills/_graph.json graph filtered by key tier-filtered
/.well-known/skills/{name}/* serveFile (catalog.ts) tier-gated
/ serveLanding (views/landing.ts) none (content filtered)

Global ?key=xxx on any GET auto-logs-in: validates key, sets snappy_auth cookie (30d, HttpOnly, Secure, SameSite=Lax), 303 redirects to same URL with key stripped.

View structure#

Each view is a TypeScript function returning an HTML string (SSR). No framework -- vanilla JS for interactivity. All views import from topbar.ts for shared header/theme.

View file URL Purpose
views/landing.ts / Card grid of all visible skills, hierarchical tier ordering (hero > orchestrators > domain > standalone) based on graph in/out degree. Each card shows: health dot (API liveness), loader pill (green "loader" text if AGENTS.md in files[]), graph badges (in/out degree), cred dots, size, updated time.
views/skill-detail.ts /skills/{name} 3-column docs layout: sidebar (anatomy/config/graph), main (rendered markdown), AI audit panel
views/console.ts /console Operator page: AI provider credential management + config map + AI models map
views/lint.ts /lint Lint dashboard: static analysis violations across all skills

Auth model (auth.ts)#

  1. getToken(request) -- reads Authorization: Bearer xxx header OR snappy_auth cookie
  2. authenticate(request, env) -- returns KeyRecord with tier + name + optional skills scope
  3. canViewSkill(key, skill) -- tier-aware visibility (single source of truth for catalog + detail)
  4. canAccessFile(env, key, skillName) -- per-file gate, reads catalog to prevent URL-guessing bypass

Token priority: Authorization header > cookie. Master key resolves to { tier: "personal", name: "robert" }. No token = { tier: "public", name: "anonymous" }.

Access tiers#

Tier Sees Key Required
public Only public skills No
client public + their scoped skills (via skills[] array) Yes
subscriber Everything except personal Yes
personal Everything Master key

Key files and roles#

File Path (relative to ~/projects/snappy-skills/src/) Role Common edits
index.ts src/index.ts Router -- dispatches to views/admin Add new route, add POST path to allowlist
types.ts src/types.ts Env, KeyRecord, SkillEntry, SkillMeta Add KV binding, extend interfaces
auth.ts src/auth.ts Token extraction, key validation, tier gating Adjust tier logic, cookie params
catalog.ts src/catalog.ts serveIndex, serveFile -- KV reads for catalog Catalog filter changes
topbar.ts src/topbar.ts Shared sticky header: logo, nav, theme toggle, session badge Add nav items, change theme vars
config-map.ts src/config-map.ts Static credential-to-skill and model-to-skill maps Add new credential, map new skill
settings.ts src/settings.ts SUPPORTED_CREDENTIALS, per-user KV CRUD Add new AI provider
graph.ts src/graph.ts Cross-skill link graph (parse SKILL.md → nodes/edges) Adjust edge parsing
lint.ts src/lint.ts Static analysis rules engine Add lint rules
ai.ts src/ai.ts AI audit proxy (streams OpenRouter response) Change model, adjust prompt
markdown.ts src/markdown.ts Markdown-to-HTML renderer (links, tables, code blocks) Fix rendering bugs
anatomy.ts src/anatomy.ts SKILL.md section parser (YAML frontmatter, headings) Adjust section detection
meta.ts src/meta.ts Display metadata (icons, colors, descriptions) Add skill display overrides
views/landing.ts src/views/landing.ts Landing page with hierarchical skill cards Card layout, tier grouping
views/skill-detail.ts src/views/skill-detail.ts 3-column detail page Sidebar panels, content rendering
views/console.ts src/views/console.ts AI Keys page + config map + models map + PID health Add console section
views/lint.ts src/views/lint.ts Lint dashboard Violation display
admin/keys.ts src/admin/keys.ts Key CRUD + graph rebuild New admin endpoints
admin/apiLiveness.ts src/admin/apiLiveness.ts Extract/probe/recheck API refs Probe logic
admin/lint.ts src/admin/lint.ts Run/status/rules lint endpoints New lint routes
wrangler.json wrangler.json Worker config: KV bindings, account ID, compatibility date Add KV namespace
scripts/publish-skill.js scripts/publish-skill.js Publish local skill dir to KV Metadata fields

Admin endpoints (all require Authorization: Bearer $MASTER_KEY)#

Key management#

bash# List all keys
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/keys

# Create key (scoped to specific skills)
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/keys/create?name=Acme&tier=client&skills=skill1,skill2&expires=2027-01-01"

# Revoke key
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/keys/revoke?key=snappy_xxx"

Graph rebuild#

bash# Rebuild cross-skill link graph (run after every publish)
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/graph/rebuild

API liveness (extract-then-probe pipeline)#

bash# Step 1: Extract all API refs from every skill's SKILL.md
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/api-liveness/extract

# Step 2: Probe hosts in chunks (free tier = 50 subrequests max)
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/api-liveness/probe?offset=0&limit=40"
# Repeat with next_offset until done=true

# Recheck a single skill's hosts (force, no cache)
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/api-liveness/recheck?skill=snappy-ops"

Lint#

bash# Run full lint scan
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/lint/run

# Get cached results
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/lint/status

# List all rules
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/lint/rules

PID feedback loop#

bash# Read PID status (pushed to KV `_pid_status` by local collector script)
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/pid/status

The /console page also renders this data server-side (reads _pid_status from SKILLS_STORE KV). The Worker is read-only -- a local script collects feedback log entries, loader coverage, queue depth, and gap signals, then pushes the JSON blob to KV.


Publishing workflow#

bash# 1. Edit skill content locally
vim ~/.claude/skills/snappy-docs/SKILL.md

# 2. Publish to KV (reads all files, uploads each, updates index.json)
cd ~/projects/snappy-skills
node scripts/publish-skill.js snappy-docs ~/.claude/skills/snappy-docs public

# 3. Rebuild the cross-skill graph
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/graph/rebuild

# 4. Verify
curl -s https://skills.snappy.ai/.well-known/skills/index.json | jq '.skills[] | select(.name=="snappy-docs")'

KV key convention: {skillName}/{filename} (e.g. snappy-docs/SKILL.md). The catalog is at key index.json. The graph is at _graph.json. Logos at _logo/dark.svg and _logo/light.svg.

Tiers on publish: third arg to publish-skill.js: public, client, subscriber, or personal.


Console page sections#

The /console page (views/console.ts) has three sections:

  1. Session -- signed-in identity, tier, masked token.
  1. Connections -- all credentials from config-map.ts grouped by provider type (AI / Platform / Infra). Each row shows: label, env var, linked skills, usage count badge, and connection status. Browser-storable keys (from SUPPORTED_CREDENTIALS in settings.ts) get inline save/clear forms; the rest show "Resolved via .env.cache". Skills with 10+ credential dependencies highlighted.
  1. System Health -- PID feedback loop status (loader coverage, corrections queue, gap signals).

To add a new console section: add HTML in renderConsole(), CSS in the <style> block, JS in the <script> block. All inline, no bundler.


CSS conventions#

All theme colors use oklch via CSS custom properties defined in topbar.ts themeStyles():

Variable Dark value (oklch) Purpose
--bg 0.2679 0.0036 106 Page background
--fg 0.8574 0.0142 93 Default text
--card 0.3085 0.0035 106 Card background
--card-fg 0.9818 0.0054 95 Card text
--primary 0.6724 0.1308 38 Accent (orange)
--muted 0.3213 0.0038 106 Muted background
--muted-fg 0.7213 0.0169 99 Muted text
--border 0.4118 0.0101 106 Borders
--green 0.72 0.17 142 Success/configured
--red 0.65 0.2 25 Error/danger

Theme toggle: html.dark (default) / html.light. Pre-paint script in <head> reads localStorage('snappy_theme') to avoid flash. Toggle function: toggleSnappyTheme() in topbarScript().

Layout patterns: card grid on landing, 3-column on detail (sidebar + main + audit), single-column on console. Max-width 880px. Fonts: Inter (body) + JetBrains Mono (code).


Deploying Worker updates#

bashcd ~/projects/snappy-skills
npx wrangler deploy

The MASTER_KEY is set as a Worker secret (not in wrangler.json). To update it:

bashcd ~/projects/snappy-skills
echo "the-key-value" | npx wrangler secret put MASTER_KEY

Rules#

  • KV rate limit (error 10048) -- wait 60s and retry. Do NOT loop. Cloudflare throttles KV writes at ~1000/sec; the publish script can hit this on bulk publishes.
  • wrangler deploy from wrong directory -- STOP. Must cd ~/projects/snappy-skills first. Wrangler reads wrangler.json from cwd.
  • Hardcoded API key in source -- REFUSE to commit. Master key is a Worker secret via wrangler secret put MASTER_KEY. Client keys live in API_KEYS KV. Never in source.
  • Subrequest budget exhausted during probe -- the free-tier 50-fetch cap was hit. Use ?offset=N&limit=40 to chunk probes across multiple invocations. The probe handler detects this and sets exhausted: true in the response.
  • Publishing without graph rebuild -- the detail page cross-references and landing page hierarchy will be stale. Always run /admin/graph/rebuild after publish.
  • Creating a client key without skills array -- they see only public skills (silently degraded). Always pass &skills=name1,name2.
  • Compatibility date bump -- do NOT change compatibility_date in wrangler.json unless tested. Worker is stable on 2025-04-01.
  • Auth env not loaded -- master key value lives in ~/.claude/skills/snappy-settings/.env.cache as SNAPPY_MASTER_KEY. Read via env("SNAPPY_MASTER_KEY") or source ~/.claude/skills/snappy-settings/scripts/load-env.sh. Never hardcode. Bitwarden was removed on 2026-04-08 -- do not reintroduce any sync layer.

Anti-patterns#

WRONG RIGHT
Editing KV directly via wrangler kv key put Use publish-skill.js -- it handles index.json + metadata
wrangler kv key list without --remote Always pass --remote -- wrangler defaults to local emulator
Publishing without version bump in .snappy-meta.json Bump per semver table in SKILL.md; publish script recomputes checksum
Editing skill content via the gateway Edit locally in ~/.claude/skills/, then publish
Adding a view without importing topbar.ts Every view must use themeStyles(), themeHeadScript(), topbarStyles(), topbarHtml(), topbarScript()
Hardcoding theme colors Use CSS variables (--bg, --card, --primary, etc.)
Adding a POST route without updating the allowlist index.ts has an explicit POST path allowlist -- add your route there
Skipping canAccessFile check on new file-serving routes All file/zip endpoints must call canAccessFile to prevent URL-guessing bypass
Caching subrequest-exhausted probe results The probe handler already skips caching these -- don't change that
Running all probes in one shot Chunk with ?offset=N&limit=40 to stay under the 50-subrequest cap

Reference (last resort)#

Full docs: ~/.claude/skills/snappy-gateway/SKILL.md, infrastructure.md, key-management.md, publishing.md. Worker source: ~/projects/snappy-skills/src/. Read them only when this AGENTS.md doesn't cover the case. Default to this file.

Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.

<!-- SKILL-INDEX-START -->

[snappy-gateway Index]|root: ~/.claude/skills/snappy-gateway|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,infrastructure.md,key-management.md,publishing.md}

<!-- SKILL-INDEX-END -->

Used by#

  • snappy-skill

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
get name read npx tsx ~/.claude/skills/snappy-gateway/api.ts get "<name>"
list read npx tsx ~/.claude/skills/snappy-gateway/api.ts list
card name read npx tsx ~/.claude/skills/snappy-gateway/api.ts card "<name>"
publish name?, tier? post npx tsx ~/.claude/skills/snappy-gateway/api.ts publish
pull name write-reversible npx tsx ~/.claude/skills/snappy-gateway/api.ts pull "<name>"

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->

---
name: snappy-gateway
role: Cloudflare Worker gateway that publishes, gates, and serves Claude Code skills at skills.snappy.ai
loaded-by: PreToolUse hook (auto-injected when "snappy-gateway" is mentioned)
---

# snappy-gateway -- Agent Loader

You manage the skills.snappy.ai Cloudflare Worker -- a KV-backed gateway that distributes Claude Code skills with tiered access control. This file is the operational contract. Everything load-bearing lives here.

## API module

```typescript
import { listSkills, getSkill, publishSkill } from "../snappy-gateway/api.ts";
```

Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-gateway/api.ts list                  # the catalogue, with each card's check line
npx tsx ~/.claude/skills/snappy-gateway/api.ts get snappy-slack      # a published skill's SKILL.md
npx tsx ~/.claude/skills/snappy-gateway/api.ts card snappy-slack     # build the listing card, publish nothing
npx tsx ~/.claude/skills/snappy-gateway/api.ts publish snappy-slack  # one skill, with its card
npx tsx ~/.claude/skills/snappy-gateway/api.ts publish --base-set    # every base skill; the base-set gate runs first
npx tsx ~/.claude/skills/snappy-gateway/api.ts pull snappy-slack     # Add it, into the person's skills root
```

THE CARD IS THE LISTING. `card.ts` parses the skill rather than describing it:
purpose, verbs with their first call, faces, agents and cross-links from the one
AGENTS.md reader; pass/fail and the failing sentences from `snappy-tool-design
lint`; "covers 9 of 174 operations of Slack, checked <date>" from the committed
vendor corpus. Nothing on a card is a claim a person typed.

THE BASE SET IS THE GATE'S ANSWER. `--base-set` publishes every skill except the
ones `scripts/gates/base-set.mjs` keeps private, read through its own
`personal-tier.mjs`. That gate runs BEFORE any upload and a red gate publishes
nothing.

## API functions

| Function | Purpose |
|----------|---------|
| `listSkills()` | List all skills from the gateway catalog (uses master key if available) |
| `getSkill(name)` | Fetch a specific skill's SKILL.md content from the gateway |
| `publishSkill(skillName, tier?)` | Publish a local skill directory to gateway KV and rebuild the cross-skill graph |

## Operating principle

The gateway is a **distribution channel**, not an authoring tool. It serves skill files from KV, gates access by tier, and provides an operator console for AI provider credentials. All skill content is authored locally in `~/.claude/skills/` and pushed to KV via the publish script. Never edit skill content through the gateway.

---

## Architecture

```
Client Request → skills.snappy.ai (Cloudflare Worker)
  ↓
  index.ts (router) → dispatches by URL path to views or admin handlers
  ↓
  auth.ts → authenticate(request, env) → KeyRecord { tier, name, skills?, expires? }
  ↓
  KV Namespaces:
    SKILLS_STORE  -- skill files + index.json + _graph.json + _logo/*.svg
    API_KEYS      -- access tokens → KeyRecord JSON
    SETTINGS_STORE -- per-user AI provider credentials (keyed by auth token)
  ↓
  Env binding: MASTER_KEY (secret, never in source)
```

### Router pattern (index.ts)

`index.ts` is a single `fetch()` handler. It matches `url.pathname` top-down:

| Path pattern | Handler | Auth |
|---|---|---|
| `/login`, `/logout` | `handleLogin`, `handleLogout` | none |
| `/console` | `serveConsole` (views/console.ts) | session required |
| `/lint` | `serveLintDashboard` (views/lint.ts) | session required |
| `/settings`, `/settings/{provider}` | inline `handleSettings` | session required |
| `/ai/audit` | `handleAiAudit` (ai.ts) | session required |
| `/admin/api-liveness/*` | `handleApiLivenessAdmin` | master key (Bearer) |
| `/admin/lint/*` | `handleLintAdmin` | master key (Bearer or cookie) |
| `/admin/keys`, `/admin/keys/*`, `/admin/graph/rebuild`, `/admin/pid/status` | `handleAdmin` | master key (Bearer) |
| `/api-liveness.json`, `/api-refs.json` | inline read from KV | none (public read) |
| `/logo/{dark,light}.svg` | KV lookup `_logo/{theme}.svg` | none |
| `/install`, `/install.sh` | KV lookup `_install.sh` | none |
| `/download/{name}.zip` | `serveZip` | tier-gated |
| `/skills/{name}` | `serveSkillDetail` (views/skill-detail.ts) | tier-gated |
| `/.well-known/skills/index.json` | `serveIndex` (catalog.ts) | tier-filtered |
| `/.well-known/skills/_graph.json` | graph filtered by key | tier-filtered |
| `/.well-known/skills/{name}/*` | `serveFile` (catalog.ts) | tier-gated |
| `/` | `serveLanding` (views/landing.ts) | none (content filtered) |

Global `?key=xxx` on any GET auto-logs-in: validates key, sets `snappy_auth` cookie (30d, HttpOnly, Secure, SameSite=Lax), 303 redirects to same URL with key stripped.

### View structure

Each view is a TypeScript function returning an HTML string (SSR). No framework -- vanilla JS for interactivity. All views import from `topbar.ts` for shared header/theme.

| View file | URL | Purpose |
|---|---|---|
| `views/landing.ts` | `/` | Card grid of all visible skills, hierarchical tier ordering (hero > orchestrators > domain > standalone) based on graph in/out degree. Each card shows: health dot (API liveness), loader pill (green "loader" text if AGENTS.md in files[]), graph badges (in/out degree), cred dots, size, updated time. |
| `views/skill-detail.ts` | `/skills/{name}` | 3-column docs layout: sidebar (anatomy/config/graph), main (rendered markdown), AI audit panel |
| `views/console.ts` | `/console` | Operator page: AI provider credential management + config map + AI models map |
| `views/lint.ts` | `/lint` | Lint dashboard: static analysis violations across all skills |

### Auth model (auth.ts)

1. `getToken(request)` -- reads `Authorization: Bearer xxx` header OR `snappy_auth` cookie
2. `authenticate(request, env)` -- returns `KeyRecord` with tier + name + optional skills scope
3. `canViewSkill(key, skill)` -- tier-aware visibility (single source of truth for catalog + detail)
4. `canAccessFile(env, key, skillName)` -- per-file gate, reads catalog to prevent URL-guessing bypass

Token priority: Authorization header > cookie. Master key resolves to `{ tier: "personal", name: "robert" }`. No token = `{ tier: "public", name: "anonymous" }`.

### Access tiers

| Tier | Sees | Key Required |
|---|---|---|
| `public` | Only `public` skills | No |
| `client` | `public` + their scoped skills (via `skills[]` array) | Yes |
| `subscriber` | Everything except `personal` | Yes |
| `personal` | Everything | Master key |

---

## Key files and roles

| File | Path (relative to `~/projects/snappy-skills/src/`) | Role | Common edits |
|---|---|---|---|
| `index.ts` | `src/index.ts` | Router -- dispatches to views/admin | Add new route, add POST path to allowlist |
| `types.ts` | `src/types.ts` | `Env`, `KeyRecord`, `SkillEntry`, `SkillMeta` | Add KV binding, extend interfaces |
| `auth.ts` | `src/auth.ts` | Token extraction, key validation, tier gating | Adjust tier logic, cookie params |
| `catalog.ts` | `src/catalog.ts` | `serveIndex`, `serveFile` -- KV reads for catalog | Catalog filter changes |
| `topbar.ts` | `src/topbar.ts` | Shared sticky header: logo, nav, theme toggle, session badge | Add nav items, change theme vars |
| `config-map.ts` | `src/config-map.ts` | Static credential-to-skill and model-to-skill maps | Add new credential, map new skill |
| `settings.ts` | `src/settings.ts` | `SUPPORTED_CREDENTIALS`, per-user KV CRUD | Add new AI provider |
| `graph.ts` | `src/graph.ts` | Cross-skill link graph (parse SKILL.md → nodes/edges) | Adjust edge parsing |
| `lint.ts` | `src/lint.ts` | Static analysis rules engine | Add lint rules |
| `ai.ts` | `src/ai.ts` | AI audit proxy (streams OpenRouter response) | Change model, adjust prompt |
| `markdown.ts` | `src/markdown.ts` | Markdown-to-HTML renderer (links, tables, code blocks) | Fix rendering bugs |
| `anatomy.ts` | `src/anatomy.ts` | SKILL.md section parser (YAML frontmatter, headings) | Adjust section detection |
| `meta.ts` | `src/meta.ts` | Display metadata (icons, colors, descriptions) | Add skill display overrides |
| `views/landing.ts` | `src/views/landing.ts` | Landing page with hierarchical skill cards | Card layout, tier grouping |
| `views/skill-detail.ts` | `src/views/skill-detail.ts` | 3-column detail page | Sidebar panels, content rendering |
| `views/console.ts` | `src/views/console.ts` | AI Keys page + config map + models map + PID health | Add console section |
| `views/lint.ts` | `src/views/lint.ts` | Lint dashboard | Violation display |
| `admin/keys.ts` | `src/admin/keys.ts` | Key CRUD + graph rebuild | New admin endpoints |
| `admin/apiLiveness.ts` | `src/admin/apiLiveness.ts` | Extract/probe/recheck API refs | Probe logic |
| `admin/lint.ts` | `src/admin/lint.ts` | Run/status/rules lint endpoints | New lint routes |
| `wrangler.json` | `wrangler.json` | Worker config: KV bindings, account ID, compatibility date | Add KV namespace |
| `scripts/publish-skill.js` | `scripts/publish-skill.js` | Publish local skill dir to KV | Metadata fields |

---

## Admin endpoints (all require `Authorization: Bearer $MASTER_KEY`)

### Key management
```bash
# List all keys
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/keys

# Create key (scoped to specific skills)
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/keys/create?name=Acme&tier=client&skills=skill1,skill2&expires=2027-01-01"

# Revoke key
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/keys/revoke?key=snappy_xxx"
```

### Graph rebuild
```bash
# Rebuild cross-skill link graph (run after every publish)
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/graph/rebuild
```

### API liveness (extract-then-probe pipeline)
```bash
# Step 1: Extract all API refs from every skill's SKILL.md
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/api-liveness/extract

# Step 2: Probe hosts in chunks (free tier = 50 subrequests max)
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/api-liveness/probe?offset=0&limit=40"
# Repeat with next_offset until done=true

# Recheck a single skill's hosts (force, no cache)
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  "https://skills.snappy.ai/admin/api-liveness/recheck?skill=snappy-ops"
```

### Lint
```bash
# Run full lint scan
curl -s -X POST -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/lint/run

# Get cached results
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/lint/status

# List all rules
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/lint/rules
```

### PID feedback loop
```bash
# Read PID status (pushed to KV `_pid_status` by local collector script)
curl -s -H "Authorization: Bearer $MASTER_KEY" \
  https://skills.snappy.ai/admin/pid/status
```

The `/console` page also renders this data server-side (reads `_pid_status` from SKILLS_STORE KV). The Worker is read-only -- a local script collects feedback log entries, loader coverage, queue depth, and gap signals, then pushes the JSON blob to KV.

---

## Publishing workflow

```bash
# 1. Edit skill content locally
vim ~/.claude/skills/snappy-docs/SKILL.md

# 2. Publish to KV (reads all files, uploads each, updates index.json)
cd ~/projects/snappy-skills
node scripts/publish-skill.js snappy-docs ~/.claude/skills/snappy-docs public

# 3. Rebuild the cross-skill graph
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/graph/rebuild

# 4. Verify
curl -s https://skills.snappy.ai/.well-known/skills/index.json | jq '.skills[] | select(.name=="snappy-docs")'
```

**KV key convention:** `{skillName}/{filename}` (e.g. `snappy-docs/SKILL.md`). The catalog is at key `index.json`. The graph is at `_graph.json`. Logos at `_logo/dark.svg` and `_logo/light.svg`.

**Tiers on publish:** third arg to publish-skill.js: `public`, `client`, `subscriber`, or `personal`.

---

## Console page sections

The `/console` page (views/console.ts) has three sections:

1. **Session** -- signed-in identity, tier, masked token.

2. **Connections** -- all credentials from `config-map.ts` grouped by provider type (AI / Platform / Infra). Each row shows: label, env var, linked skills, usage count badge, and connection status. Browser-storable keys (from `SUPPORTED_CREDENTIALS` in settings.ts) get inline save/clear forms; the rest show "Resolved via .env.cache". Skills with 10+ credential dependencies highlighted.

3. **System Health** -- PID feedback loop status (loader coverage, corrections queue, gap signals).

To add a new console section: add HTML in `renderConsole()`, CSS in the `<style>` block, JS in the `<script>` block. All inline, no bundler.

---

## CSS conventions

All theme colors use oklch via CSS custom properties defined in `topbar.ts` `themeStyles()`:

| Variable | Dark value (oklch) | Purpose |
|---|---|---|
| `--bg` | `0.2679 0.0036 106` | Page background |
| `--fg` | `0.8574 0.0142 93` | Default text |
| `--card` | `0.3085 0.0035 106` | Card background |
| `--card-fg` | `0.9818 0.0054 95` | Card text |
| `--primary` | `0.6724 0.1308 38` | Accent (orange) |
| `--muted` | `0.3213 0.0038 106` | Muted background |
| `--muted-fg` | `0.7213 0.0169 99` | Muted text |
| `--border` | `0.4118 0.0101 106` | Borders |
| `--green` | `0.72 0.17 142` | Success/configured |
| `--red` | `0.65 0.2 25` | Error/danger |

Theme toggle: `html.dark` (default) / `html.light`. Pre-paint script in `<head>` reads `localStorage('snappy_theme')` to avoid flash. Toggle function: `toggleSnappyTheme()` in `topbarScript()`.

Layout patterns: card grid on landing, 3-column on detail (sidebar + main + audit), single-column on console. Max-width 880px. Fonts: Inter (body) + JetBrains Mono (code).

---

## Deploying Worker updates

```bash
cd ~/projects/snappy-skills
npx wrangler deploy
```

The `MASTER_KEY` is set as a Worker secret (not in wrangler.json). To update it:
```bash
cd ~/projects/snappy-skills
echo "the-key-value" | npx wrangler secret put MASTER_KEY
```

---

## Rules

- **KV rate limit (error 10048)** -- wait 60s and retry. Do NOT loop. Cloudflare throttles KV writes at ~1000/sec; the publish script can hit this on bulk publishes.
- **`wrangler deploy` from wrong directory** -- STOP. Must `cd ~/projects/snappy-skills` first. Wrangler reads `wrangler.json` from cwd.
- **Hardcoded API key in source** -- REFUSE to commit. Master key is a Worker secret via `wrangler secret put MASTER_KEY`. Client keys live in API_KEYS KV. Never in source.
- **Subrequest budget exhausted during probe** -- the free-tier 50-fetch cap was hit. Use `?offset=N&limit=40` to chunk probes across multiple invocations. The probe handler detects this and sets `exhausted: true` in the response.
- **Publishing without graph rebuild** -- the detail page cross-references and landing page hierarchy will be stale. Always run `/admin/graph/rebuild` after publish.
- **Creating a `client` key without `skills` array** -- they see only public skills (silently degraded). Always pass `&skills=name1,name2`.
- **Compatibility date bump** -- do NOT change `compatibility_date` in wrangler.json unless tested. Worker is stable on `2025-04-01`.
- **Auth env not loaded** -- master key value lives in `~/.claude/skills/snappy-settings/.env.cache` as `SNAPPY_MASTER_KEY`. Read via `env("SNAPPY_MASTER_KEY")` or `source ~/.claude/skills/snappy-settings/scripts/load-env.sh`. Never hardcode. Bitwarden was removed on 2026-04-08 -- do not reintroduce any sync layer.

---

## Anti-patterns

| WRONG | RIGHT |
|---|---|
| Editing KV directly via `wrangler kv key put` | Use `publish-skill.js` -- it handles index.json + metadata |
| `wrangler kv key list` without `--remote` | Always pass `--remote` -- wrangler defaults to local emulator |
| Publishing without version bump in `.snappy-meta.json` | Bump per semver table in SKILL.md; publish script recomputes checksum |
| Editing skill content via the gateway | Edit locally in `~/.claude/skills/`, then publish |
| Adding a view without importing `topbar.ts` | Every view must use `themeStyles()`, `themeHeadScript()`, `topbarStyles()`, `topbarHtml()`, `topbarScript()` |
| Hardcoding theme colors | Use CSS variables (`--bg`, `--card`, `--primary`, etc.) |
| Adding a POST route without updating the allowlist | `index.ts` has an explicit POST path allowlist -- add your route there |
| Skipping `canAccessFile` check on new file-serving routes | All file/zip endpoints must call `canAccessFile` to prevent URL-guessing bypass |
| Caching subrequest-exhausted probe results | The probe handler already skips caching these -- don't change that |
| Running all probes in one shot | Chunk with `?offset=N&limit=40` to stay under the 50-subrequest cap |

---

## Reference (last resort)

Full docs: `~/.claude/skills/snappy-gateway/SKILL.md`, `infrastructure.md`, `key-management.md`, `publishing.md`. Worker source: `~/projects/snappy-skills/src/`. Read them only when this AGENTS.md doesn't cover the case. Default to this file.


Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.

<!-- SKILL-INDEX-START -->
[snappy-gateway Index]|root: ~/.claude/skills/snappy-gateway|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,infrastructure.md,key-management.md,publishing.md}
<!-- SKILL-INDEX-END -->

## Used by

- `snappy-skill`

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `get` | `name` | `read` | `npx tsx ~/.claude/skills/snappy-gateway/api.ts get "<name>"` |
| `list` | — | `read` | `npx tsx ~/.claude/skills/snappy-gateway/api.ts list` |
| `card` | `name` | `read` | `npx tsx ~/.claude/skills/snappy-gateway/api.ts card "<name>"` |
| `publish` | `name?`, `tier?` | `post` | `npx tsx ~/.claude/skills/snappy-gateway/api.ts publish` |
| `pull` | `name` | `write-reversible` | `npx tsx ~/.claude/skills/snappy-gateway/api.ts pull "<name>"` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?