snowpea-registry
The private skill registry behind https://registry.snowpea.ai — browse, search, rate and install skills for the snowpea agent. One Node process serves the JSON API and the Vue SPA; state is a single SQLite file on a mounted volume.
snowpea skill install registry:ralplan한국어
무엇인가
snowpea 에이전트가 쓰는 스킬(SKILL.md 와 그 옆의 파일들)의 레지스트리입니다. 공개 코어의 skills/registry_client.py 와 TUI/IDE 통합 검색이 이 API를 사용합니다. API 계약 전문은 docs/design/registry-contract.md 에 있습니다.
구성
| 경로 | 내용 |
|---|---|
api/src/ | Fastify 서버, SQLite 접근 계층, 관리 CLI, 시드 스크립트 |
api/src/lib/sources/ | 연합 허브 어댑터 |
api/test/ | vitest 테스트 (인메모리 SQLite + 고정 픽스처 + 부팅 스모크) |
web/src/ | Vue 3 + Vite SPA (같은 프로세스가 정적 파일로 서빙) |
seed/skills/ | 데모용 스킬 6개 |
deploy/ | duho 호스트 배포 스크립트 |
docs/design/ | API 계약 문서 |
개발
nvm use # .nvmrc = 24. Node 22 이상이면 됩니다
npm install
DB_PATH=./data/registry.db npm run seed # 데모 스킬 6개 주입
DB_PATH=./data/registry.db npm run dev # API :8060 + Vite :5173
npm test # 138개 테스트
npm run typecheck연합 소스 (federated sources)
이 색인에는 다른 공개 스킬 허브도 함께 들어옵니다. 백그라운드 크롤러가 각 허브의 카탈로그를 미러링하고, 검색·목록의 모든 항목은 출처(source)를 함께 들고 있습니다.
| 소스 | 상태 | 카탈로그 출처 |
|---|---|---|
local | 사용 | 이 레지스트리에 배포된 스킬 |
clawhub | 사용 | ClawHub 의 /v1/feeds/{skills,plugins} |
claude-marketplaces | 사용 | 설정된 GitHub 저장소의 marketplace.json |
agentskills | 비활성 | agentskills.io 는 카탈로그가 아니라 명세 문서 사이트 |
hermes | 비활성 | hermes-hub.ai 도메인이 존재하지 않음 |
ClawHub 의 robots.txt 는 /api/ 를 막고 /v1/feeds/* 를 허용하므로, 어댑터는 피드만 읽습니다 — 카탈로그당 요청 1회, HTML 스크래핑 없음, 식별 가능한 User-Agent 사용. 비활성 허브 두 곳도 코어가 가정하는 형태로 구현되어 있으며, GET /v1/sources 에 실제 사유가 그대로 나옵니다. AGENTSKILLS_ENDPOINT 나 HERMES_ENDPOINT 를 지정하면 코드 변경 없이 켜집니다.
GET /v1/skills/{encodedId}/download 는 로컬이든 연합이든 모든 id 를 처리하므로, 코어는 이 엔드포인트 하나로 무엇이든 설치할 수 있습니다. GitHub 마켓플레이스 플러그인은 저장소 아카이브에서 해당 디렉터리만 뽑아 다시 압축해 돌려주고(캐시: 최초 831ms, 이후 5ms), 가져올 아카이브가 없는 허브는 501 과 함께 사유와 대안을 알려줍니다. 연합 id 에는 :, /, @ 가 들어가므로 id 전체를 하나의 경로 세그먼트로 퍼센트 인코딩해야 합니다.
크롤은 MIRROR_INTERVAL_MIN 분마다(기본 60), 부팅 후 MIRROR_BOOT_DELAY_MS (기본 10초) 뒤 처음 실행됩니다. MIRROR_INTERVAL_MIN=0 이면 끕니다. 허브가 실패하면 마지막 성공분을 그대로 유지하고, /v1/sources 가 오래된 데이터임을 알려줍니다. 배포·평점·버전 기록은 로컬 스킬 전용입니다.
런타임 요구사항: Node 22 이상
저장소는 Node 내장 node:sqlite 를 씁니다(Node 22 도입). 네이티브 모듈이 없으므로 빌드 도구도, 재빌드도, ABI 불일치도 없습니다. package.json 의 engines 와 .npmrc 의 engine-strict=true 가 낮은 런타임에서의 설치를 명확한 메시지와 함께 거절합니다. Node 20 은 지원하지 않습니다 — better-sqlite3 11 은 Node 26 의 V8 에서 컴파일되지 않고, better-sqlite3 13 은 스스로 Node 22 이상을 요구하므로 Node 20 과 Node 26 을 동시에 만족시키는 경로가 없습니다.
배포 (duho, 192.168.100.61:8060)
cp .env.example .env && $EDITOR .env # ADMIN_TOKEN 설정
./deploy/deploy-duho.sh --dry-run # 실행할 명령만 출력
./deploy/deploy-duho.sh # 빌드 → ssh 로 이미지 전송 → compose up
./deploy/seed-remote.sh snowpea # (최초 1회) 시드 + 배포자 토큰 발급이미지는 레지스트리를 거치지 않고 docker save | ssh | docker load 로 전달됩니다. TLS 와 리버스 프록시(registry.snowpea.ai → 192.168.100.61:8060)는 이 레포 밖에서 설정합니다.
디자인 (제품군 공통)
레지스트리는 snowpea 제품군의 한 표면입니다. 상단 바와 푸터의 제품 스위처는 Agent · Registry 두 곳만 보여줍니다 — 다른 제품은 링크하지 않습니다. 서체와 형태 값은 스튜디오(snowpea studio)에서, 색 역할은 snowpea desktop 에서 가져와 한 제품군으로 읽히게 했습니다.
- 색 역할과 이름은 snowpea desktop 의 계약을 그대로 씁니다 (
snowpea-ide/src/renderer/theme/tokens.css,themes/snowpea-{dark,light}.ts). 저쪽에서 값이 바뀌면 그대로 옮겨오면 됩니다. - 다크가 기본입니다. 라이트는 토글로 선택하며 선택은 유지되고, 첫 페인트 전에 적용되어 깜빡임이 없습니다.
- 브랜드 램프는
--pea-200#a8f0c6→--pea-400#3ddc84→--pea-600#1e9e6a이며, 이름은 브랜드 README 를 따릅니다. - 서체: Montserrat(디스플레이, 스튜디오와 동일), Pretendard(본문, 한·영), JetBrains Mono(기계 어휘 — id·버전·태그·설치 명령). 모두 앱이 직접 서빙하므로 CDN 없이 그려집니다.
- 반경·바 높이는 스튜디오 값(
--r-card: 10px,--r-input: 4px,--bar-h: 56px). - 마크와 로고는 공용 브랜드(
snowpea-site/shared/brand/)의 새싹 심볼입니다 — WAFOUR 계열의 모노라인 도형. 헤더는[새싹] snowpea REGISTRY락업을 씁니다. 그라디언트는 페이지당 한 번, 헤더 마크에만 씁니다(brand README 규칙 1). 도형의 원본은 그 레포의shared/marks.js이므로 SVG 를 직접 고치지 마세요. - 링크 카드(1200×630)와 래스터 아이콘은
npm run og로 다시 렌더합니다:web/public/{og.jpg,apple-touch-icon.png,icon-512.png}.
관리 CLI
npm run admin -- token create <publisher> --name "표시 이름" # 토큰 발급 (한 번만 표시)
npm run admin -- token list [publisher]
npm run admin -- token revoke <tokenId>
npm run admin -- curate add <skillId> --collection featured
npm run admin -- skill list | skill remove <skillId>토큰은 SHA-256 해시로만 저장되므로 발급 시점에 받아 두어야 합니다.
스킬 배포
export SNOWPEA_REGISTRY_TOKEN=spr_…
snowpea skill publish ./my-skill
# 또는
curl -X POST https://registry.snowpea.ai/v1/skills \
-H "Authorization: Bearer $SNOWPEA_REGISTRY_TOKEN" \
-F "file=@my-skill.zip"레지스트리는 루트의 SKILL.md, 프런트매터(name, description, version), 경로 안전성, 5MB 용량 한도를 검사합니다.
English
What it is
A registry for snowpea skills — a SKILL.md and the files beside it. The public core's skills/registry_client.py and the unified search in the TUI/IDE read this API. The full contract is in docs/design/registry-contract.md.
Layout
| path | contents |
|---|---|
api/src/ | Fastify server, SQLite layer, admin CLI, seed script |
api/src/lib/sources/ | one adapter per federated hub |
api/test/ | vitest suites (in-memory SQLite, recorded fixtures, boot smoke) |
web/src/ | Vue 3 + Vite SPA, served as static files by the same process |
seed/skills/ | six demo skills |
deploy/ | deployment scripts for the duho host |
docs/design/ | the API contract |
Development
nvm use # .nvmrc = 24; anything from Node 22 up works
npm install
DB_PATH=./data/registry.db npm run seed # six demo skills
DB_PATH=./data/registry.db npm run dev # API on :8060, Vite on :5173
npm test # 138 tests
npm run typecheckThe Vite dev server proxies /v1 and /healthz to the API, so the SPA behaves the same in development as it does behind the single production process.
Federated sources
The index is not only its own. A background crawler mirrors other public skill hubs, and every result — in search and in the lists — says which hub it came from.
| source | state | where the catalog comes from |
|---|---|---|
local | live | skills published to this registry |
clawhub | live | ClawHub's own /v1/feeds/{skills,plugins} |
claude-marketplaces | live | marketplace.json in configured GitHub repos |
agentskills | disabled | agentskills.io publishes a spec, not a catalog |
hermes | disabled | hermes-hub.ai does not resolve |
ClawHub's robots.txt disallows /api/ and allows /v1/feeds/*, so the adapter reads the feeds — one request per catalog, no scraping, and an identifying User-Agent. The two disabled hubs are implemented against the shape the core assumes and carry their real reason in GET /v1/sources; setting AGENTSKILLS_ENDPOINT or HERMES_ENDPOINT turns one on.
curl 'https://registry.snowpea.ai/v1/skills?q=calendar' # local + mirrored
curl 'https://registry.snowpea.ai/v1/skills?sources=local' # this registry only
curl 'https://registry.snowpea.ai/v1/skills?q=calendar&live=1' # also ask the hubs now
curl 'https://registry.snowpea.ai/v1/sources' # per-hub health
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
https://registry.snowpea.ai/v1/admin/mirror/run # crawl nowThe crawl runs every MIRROR_INTERVAL_MIN minutes (default 60), first after MIRROR_BOOT_DELAY_MS (default 10s); MIRROR_INTERVAL_MIN=0 turns it off. A hub that fails keeps its last good rows, and /v1/sources says they are stale. MARKETPLACE_REPOS=owner/repo,other/repo@branch:path.json configures which GitHub marketplaces are read.
GET /v1/skills/{encodedId}/download serves any id, local or federated, so one endpoint installs everything. A GitHub marketplace plugin is lifted out of its repository archive and re-zipped (cached; 831 ms cold, 5 ms warm). A hub that publishes nothing fetchable answers 501 with the reason and the alternative — ClawHub distributes through its own client, and its package API sits behind the /api/ prefix its robots.txt disallows.
A federated id contains :, / and @, so encode the whole id as one path segment:
curl -o plugin.zip \
'https://registry.snowpea.ai/v1/skills/claude-marketplaces%3Aanthropics%2Fclaude-code%40agent-sdk-dev/download'Publishing, ratings and version history stay local-only. A federated SKILL.md is proxied and cached; ClawHub publishes its body only as a rendered page, so what comes back there is reconstructed from that markup and is labelled x-snowpea-readme-origin: rendered.
Runtime requirement: Node 22 or newer
Storage is Node's built-in node:sqlite, which arrived in Node 22. There is no native module, so there is nothing to rebuild, no C++ toolchain in the image, and no ABI mismatch when the Node version changes. engines in package.json and engine-strict=true in .npmrc refuse an install on an older runtime with a clear message rather than failing later.
Node 20 is not supported. It cannot be: better-sqlite3 11 no longer compiles against Node 26's V8 (GetPrototype, Context::GetIsolate and PropertyCallbackInfo::This are gone), better-sqlite3 13 declares engines.node >= 22 itself, and node:sqlite does not exist before 22 — so no single storage layer spans Node 20 and Node 26.
API at a glance
| method | path | notes |
|---|---|---|
| GET | /v1/skills?q=&tag=&sort=&page=&per_page=&sources=&live= | federated; sources = all | local | comma list |
| GET | /v1/sources | per-hub health and mirrored counts |
| POST | /v1/admin/mirror/run | admin token; crawl every hub now |
| GET | /v1/skills/{id} | latest version, readme, versions, ratings, publisher |
| GET | /v1/skills/{id}/versions/{v} | one version and its file list |
| GET | /v1/skills/{id}/SKILL.md?v= | raw markdown |
| GET | /v1/skills/{id}/download?v= | zip; increments the download counters |
| GET | /v1/tags, /v1/curated, /v1/publishers/{id} | index pages |
| GET | /v1/meta, /healthz | version and counts, liveness |
| POST | /v1/skills | Bearer publisher token; multipart zip or JSON files |
| POST | /v1/skills/{id}/rating | {stars: 1-5, comment?}; token or anonymous |
Base URL for the core: https://registry.snowpea.ai/v1. Install specs are registry:<id>.
Deployment (duho, 192.168.100.61:8060)
cp .env.example .env && $EDITOR .env # set ADMIN_TOKEN
./deploy/deploy-duho.sh --dry-run # print every command, run none
./deploy/deploy-duho.sh # build → ship over ssh → compose up
./deploy/seed-remote.sh snowpea # once: seed and issue a tokenThe image never touches a container registry: it travels as docker save | gzip | ssh | docker load. The stack mounts ./data:/data, so the database survives redeploys. TLS and the reverse proxy (registry.snowpea.ai → 192.168.100.61:8060) live outside this repo.
Environment:
| variable | default | meaning |
|---|---|---|
PORT / HOST | 8060 / 0.0.0.0 | listen address |
DB_PATH | /data/registry.db | SQLite file |
REGISTRY_BASE_URL | http://localhost:8060 | origin used in installUrl and OG tags |
ADMIN_TOKEN | generated | bearer token for admin-only operations |
MAX_UPLOAD_BYTES | 5242880 | package size cap |
WEB_DIST | /app/web/dist | built SPA |
MIRROR_INTERVAL_MIN | 60 | minutes between hub crawls; 0 disables |
MIRROR_BOOT_DELAY_MS | 10000 | delay before the first crawl |
MARKETPLACE_REPOS | two defaults | owner/repo[@branch][:path], comma separated |
AGENTSKILLS_ENDPOINT, HERMES_ENDPOINT | unset | set either to enable that hub |
ARCHIVE_CACHE_DIR | <db dir>/archives | built federated archives |
ARCHIVE_TTL_MIN | 360 | how long a built archive stays fresh |
MAX_REPO_ARCHIVE_BYTES | 62914560 | cap on a repository archive download |
REGISTRY_USER_AGENT | identifies this registry | sent to every hub |
BUILTIN_SKILLS | deep-interview,deep-research,ralplan | comma-separated local skill ids that ship inside snowpea; empty string means none |
The image is node:24-alpine in both stages and carries no compiler: with the storage layer built into Node there is nothing to compile. Node 24 loads node:sqlite without the experimental warning that Node 22 prints.
Design (shared across the series)
The registry is one surface of the snowpea series. The product switcher in the top bar and the footer carries Agent · Registry only — no other product is linked. The type and geometry come from snowpea studio and the colour roles from snowpea desktop, so the surfaces read as one family.
- Colour roles and their names are snowpea desktop's contract (
snowpea-ide/src/renderer/theme/tokens.cssandthemes/snowpea-{dark,light}.ts), so a value changed there carries over verbatim. - Dark is the default. Light is a choice the visitor makes; it persists and is applied before the first paint, so there is no flash.
- The brand ramp is
--pea-200#a8f0c6→--pea-400#3ddc84→--pea-600#1e9e6a, named as the brand README names it.--pea-400is the accent on dark; light uses the deeper#0f5f2cfrom the desktop theme. - Type: Montserrat for display (the studio's face), Pretendard for prose in Korean and English, JetBrains Mono for the machine's vocabulary — ids, versions, tags, install lines. All three are served by this app, so the page draws without a CDN.
- Radii and bar height are the studio's (
--r-card: 10px,--r-input: 4px,--bar-h: 56px). - The mark is the shared snowpea sprout from
snowpea-site/shared/brand/: a WAFOUR-language monoline symbol — stem, leaf, open ring for the pea, and WAFOUR's dot as a second pea. The header sets the[sprout] snowpea REGISTRYlockup; only the tag differs across the series. The gradient appears once per page, on that mark (brand README rule 1) — everything else, including the hero specimen's edge, is flat--accent. The geometry's source isshared/marks.jsin the site repo; never hand-edit the SVG. npm run ogre-rendersweb/public/{og.jpg,apple-touch-icon.png,icon-512.png}fromweb/og/card.htmlandweb/public/favicon.svg— real pages built from these same tokens, so a brand change is a re-render, not a redraw. (Needs Chrome and ImageMagick; the deployed image ships the rendered files.)
Tokens live in web/src/styles/tokens.css; no component hard-codes a colour.
Admin CLI
npm run admin -- token create <publisher> --name "Display Name"
npm run admin -- token list [publisher]
npm run admin -- token revoke <tokenId>
npm run admin -- publisher add <id> --github user --bio "…"
npm run admin -- curate add <skillId> --collection featured
npm run admin -- skill list | skill remove <skillId>Tokens are stored as a SHA-256 hash, so the value is printed once and never again.
Publishing a skill
A skill is a directory with SKILL.md at its root:
my-skill/
├── SKILL.md
└── references/style.md---
name: my-skill
description: One sentence a reader can act on, 8-500 characters.
version: 0.1.0
tags: [review, git]
license: MIT
---export SNOWPEA_REGISTRY_TOKEN=spr_…
snowpea skill publish ./my-skillThe registry checks the frontmatter, path safety and the size cap, and refuses a version number that already exists — bump it before republishing.
Security notes
- Publisher tokens are hashed at rest; the admin token is compared in constant time.
- Uploaded paths are rejected if absolute, backslashed, NUL-bearing or containing
.., so no client can be made to write outside the skill directory. - Rendered SKILL.md is sanitized twice: markdown-it runs with raw HTML disabled, and the output passes through DOMPurify.
- Anonymous ratings are rate limited and deduplicated per IP hash; raw IPs are never stored.
- The container runs as a non-root user with only
/datawritable.