Skip to content

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 계약 문서

개발 ​

bash
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) ​

bash
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 ​

bash
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 해시로만 저장되므로 발급 시점에 받아 두어야 합니다.

스킬 배포 ​

bash
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 ​

pathcontents
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 ​

bash
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 typecheck

The 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.

sourcestatewhere the catalog comes from
localliveskills published to this registry
clawhubliveClawHub's own /v1/feeds/{skills,plugins}
claude-marketplaceslivemarketplace.json in configured GitHub repos
agentskillsdisabledagentskills.io publishes a spec, not a catalog
hermesdisabledhermes-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.

bash
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 now

The 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:

bash
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 ​

methodpathnotes
GET/v1/skills?q=&tag=&sort=&page=&per_page=&sources=&live=federated; sources = all | local | comma list
GET/v1/sourcesper-hub health and mirrored counts
POST/v1/admin/mirror/runadmin 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, /healthzversion and counts, liveness
POST/v1/skillsBearer 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) ​

bash
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 token

The 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:

variabledefaultmeaning
PORT / HOST8060 / 0.0.0.0listen address
DB_PATH/data/registry.dbSQLite file
REGISTRY_BASE_URLhttp://localhost:8060origin used in installUrl and OG tags
ADMIN_TOKENgeneratedbearer token for admin-only operations
MAX_UPLOAD_BYTES5242880package size cap
WEB_DIST/app/web/distbuilt SPA
MIRROR_INTERVAL_MIN60minutes between hub crawls; 0 disables
MIRROR_BOOT_DELAY_MS10000delay before the first crawl
MARKETPLACE_REPOStwo defaultsowner/repo[@branch][:path], comma separated
AGENTSKILLS_ENDPOINT, HERMES_ENDPOINTunsetset either to enable that hub
ARCHIVE_CACHE_DIR<db dir>/archivesbuilt federated archives
ARCHIVE_TTL_MIN360how long a built archive stays fresh
MAX_REPO_ARCHIVE_BYTES62914560cap on a repository archive download
REGISTRY_USER_AGENTidentifies this registrysent to every hub
BUILTIN_SKILLSdeep-interview,deep-research,ralplancomma-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.css and themes/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-400 is the accent on dark; light uses the deeper #0f5f2c from 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 REGISTRY lockup; 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 is shared/marks.js in the site repo; never hand-edit the SVG.
  • npm run og re-renders web/public/{og.jpg,apple-touch-icon.png,icon-512.png} from web/og/card.html and web/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 ​

bash
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
yaml
---
name: my-skill
description: One sentence a reader can act on, 8-500 characters.
version: 0.1.0
tags: [review, git]
license: MIT
---
bash
export SNOWPEA_REGISTRY_TOKEN=spr_…
snowpea skill publish ./my-skill

The 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 /data writable.

MIT licensed. The manual is generated from the product repositories — see the banner on each page.