snappy-gateway skill
get namereadlistreadcard namereadpublish name? tier?postpull namewrite-reversible/client/v4/accounts/91e4e6d01964d00df36b85bd7e1549eb/workers/domains/client/v4/accounts/91e4e6d01964d00df36b85bd7e1549eb/workers/domains$ npx snappy-skills install snappy-gateway
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.
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.
| 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 |
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.
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)
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.
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 |
getToken(request) -- reads Authorization: Bearer xxx header OR snappy_auth cookieauthenticate(request, env) -- returns KeyRecord with tier + name + optional skills scopecanViewSkill(key, skill) -- tier-aware visibility (single source of truth for catalog + detail)canAccessFile(env, key, skillName) -- per-file gate, reads catalog to prevent URL-guessing bypassToken priority: Authorization header > cookie. Master key resolves to { tier: "personal", name: "robert" }. No token = { tier: "public", name: "anonymous" }.
| 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 |
| 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 |
Authorization: Bearer $MASTER_KEY)#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"
bash# Rebuild cross-skill link graph (run after every publish)
curl -s -H "Authorization: Bearer $MASTER_KEY" https://skills.snappy.ai/admin/graph/rebuild
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"
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
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.
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.
The /console page (views/console.ts) has three sections:
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.To add a new console section: add HTML in renderConsole(), CSS in the <style> block, JS in the <script> block. All inline, no bundler.
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).
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
wrangler deploy from wrong directory -- STOP. Must cd ~/projects/snappy-skills first. Wrangler reads wrangler.json from cwd.wrangler secret put MASTER_KEY. Client keys live in API_KEYS KV. Never in source.?offset=N&limit=40 to chunk probes across multiple invocations. The probe handler detects this and sets exhausted: true in the response./admin/graph/rebuild after publish.client key without skills array -- they see only public skills (silently degraded). Always pass &skills=name1,name2.compatibility_date in wrangler.json unless tested. Worker is stable on 2025-04-01.~/.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.| 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 |
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 -->
snappy-skill<!-- SNAPPY-CONTRACT-VERBS-START -->
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>" |
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 -->