SkillX.sh — AI agent skills marketplace. Web marketplace + CLI + hybrid search engine. "The Only Skill That Your AI Agent Needs."
Related documents:
- Project initial brief:
./docs/project-initial-brief.md - Project overview:
./docs/project-overview-pdr.md - Project roadmap:
./docs/project-roadmap-pdr.md - Design guidelines:
./docs/design-guidelines.md
Code standards: ./docs/code-standards.md
Codebase summary: ./docs/codebase-summary.md
- Monorepo: pnpm workspaces (
apps/web,packages/cli) - Web: React Router v7 + Vite + SSR on Cloudflare Workers
- Database: Cloudflare D1 (SQLite) + Drizzle ORM
- Search: FTS5 + Vectorize (768-dim, bge-base-en-v1.5) + RRF fusion
- Auth: Better Auth + GitHub OAuth (route:
api/auth/*) - Cache: Cloudflare KV, 5min TTL (home stats, leaderboard, llms.txt bodies); search results are not cached
- Storage: R2 (assets), Workers AI (embeddings)
- CSS: Tailwind v4 +
@themetokens (dark-only, mint accent#00E5A0) - CLI: Commander.js + chalk + ora + conf (package name:
skillx-sh, bin name:skillx)
apps/web/app/
├── routes/ # React Router pages + API handlers
│ ├── home.tsx # Home page + leaderboard
│ ├── skill-detail.tsx # Skill page + ratings/reviews
│ ├── profile.tsx # User favorites
│ ├── settings.tsx # API key management
│ ├── auth-catchall.tsx# Better Auth handler
│ ├── api.search.ts # Hybrid search API
│ ├── api.admin.seed.ts# Demo data seeding
│ └── api.skill-*.ts # Rate, review, favorite APIs
├── components/ # UI components
│ ├── layout/ # Navbar, footer
│ ├── search-command-palette.tsx # Cmd+K modal
│ └── leaderboard-table.tsx # Sortable table
├── lib/
│ ├── db/ # schema.ts (Drizzle), queries
│ ├── auth/ # auth-server.ts, session-helpers.ts, api-key-utils.ts
│ ├── github/ # fetch-github-skill, scan-github-repo (Tree API)
│ ├── search/ # hybrid-search, vector-search, fts5-search, rrf-fusion, boost-scoring
│ ├── vectorize/ # embed-text, chunk-text, index-skill
│ └── cache/ # KV caching utilities
├── root.tsx # App shell
└── app.css # Tailwind + theme tokens
plugins/skillx/ # Standalone Claude plugin (skill + MCP + evals), docs/claude-plugin.md
packages/cli/src/ # `skillx` npm package
├── commands/ # search, use, publish, report, config
├── lib/ # api-client, config-store
└── index.ts # Commander.js entry
pnpm install # Install all dependencies
pnpm dev # Start dev server (apps/web) → http://localhost:5173
pnpm build # Build for production
pnpm typecheck # TypeScript check (every workspace package)
pnpm test # Run unit tests (vitest) once
pnpm test:watch # Run unit tests in watch mode
pnpm seed # Seed DB (requires ADMIN_SECRET env var)
# Inside apps/web:
pnpm db:generate # Generate Drizzle migrations
pnpm db:migrate # Apply migrations locally
pnpm db:migrate:remote # Apply migrations to remote D1| Pattern | File | Auth |
|---|---|---|
/ |
home.tsx | None |
/skills/:slug |
skill-detail.tsx | None |
/skills/<slug>.md |
lib/markdown/skill-markdown-handler.ts, dispatched in workers/app.ts (React Router has no partial dynamic segment) |
None; always anonymous, public cache |
/llms.txt, /llms-full.txt |
llms-txt.ts, llms-full-txt.ts (?limit= snaps to fixed sizes; KV-cached per origin and size) |
None; protected bodies withheld |
/docs/harness, /docs/publishers |
docs-harness.tsx, docs-publishers.tsx | None |
/api/mcp |
api.mcp.ts (read-only MCP over JSON-RPC: search_packages, get_package, get_release, check_compatibility, get_kit; 64 KiB body cap, RELEASE_READ_LIMITER under mcp:) |
None; only get_kit reads an unscoped Key/Session (owner view) |
/profile |
profile.tsx | Session |
/settings |
settings.tsx | Session |
/dashboard |
dashboard.tsx (creator dashboard: packages with visibility, version history, yank, delete/restore; kits with revisions, delete/restore; publish-CLI panel; a flag-off section says "unavailable") | Session |
/api/auth/* |
auth-catchall.tsx | Better Auth |
/api/search |
api.search.ts | None |
/api/skills/:slug |
api.skill-detail.ts | None |
/api/skills/:slug/rate |
api.skill-rate.ts | Session, same-origin only |
/api/skills/:slug/review |
api.skill-review.ts | None (GET), Session same-origin only (POST) |
/api/skills/:slug/favorite |
api.skill-favorite.ts | Session, same-origin only |
/api/skills/:slug/install |
api.skill-install.ts | Optional (API key or X-Device-Id) |
/api/skills/:slug/vote |
api.skill-vote.ts | Session, same-origin only |
/api/report |
api.usage-report.ts | API key |
/api/user/api-keys |
api.user-api-keys.ts | Session |
/api/user/interactions |
api.user-interactions.ts | Session (graceful fallback) |
/api/user/agentkit-link |
api.user-agentkit-link.ts (GET link status, DELETE unlink + revoke at the AS) | Session; DELETE same-origin only |
/auth/agentkit/start, /auth/agentkit/callback |
auth.agentkit.ts ("Link with AgentKit": auth code + S256 PKCE, resource=https://skillx.sh/api) |
Session (404 unless SKILLX_AGENTKIT_IDENTITY on) |
/oauth/client.json, /.well-known/oauth-protected-resource[/api] |
oauth.client-json.ts, well-known.oauth-protected-resource.ts (CIMD, RFC 9728 metadata) | None (404 unless SKILLX_AGENTKIT_IDENTITY on) |
/api/skills/register |
api.skill-register.ts | Session (same-origin only)/Key |
/api/admin/seed |
api.admin.seed.ts | Admin secret |
/api/v1/packages/:scope/:slug/releases |
api.v1.package-releases.ts (GET list); action re-exported from api.v1.release-publish.ts (POST publish) | GET: optional Key/Session; POST: scoped key releases:publish:@handle (404 unless SKILLX_RELEASES_API on) |
/api/v1/packages/:scope/:slug/releases/:version, /api/v1/releases/:digest/manifest |
api.v1.release-manifest.ts | Optional Key/Session (404 unless SKILLX_RELEASES_API on) |
/api/v1/releases/:digest/files/*, /api/v1/releases/:digest/artifact |
api.v1.release-file.ts, api.v1.release-artifact.ts (digest reads take ?version= and ?package= selectors) |
Optional Key/Session (404 unless SKILLX_RELEASES_API on) |
/api/v1/packages/:scope/:slug/releases/:version/status |
api.v1.release-status.ts (GET signed skillx.release-status/v1, lazily issued) |
Optional Key/Session (404 unless SKILLX_RELEASES_API on) |
/api/v1/packages/:scope/:slug/releases/:version/state, /api/v1/publishers/:handle/state |
api.v1.release-state.ts (POST compare-and-set state change, audited) | Admin X-Admin-Secret or owner session/full API key; scoped keys refused (404 unless SKILLX_RELEASES_API on) |
/api/v1/packages/:scope/:slug/visibility, .../delete, .../restore |
api.v1.package-manage.ts → lib/creator/ (POST; visibility is a compare-and-set {from,to,approve?}, widening (out of private, or to public) only from a web session with approve: true; an admin delete is restorable only by the admin; soft delete with a 30-day restore, then 410; audited in package_audit; see docs/api-reference.md#managing-packages-and-kits-owner) |
Owner session or unscoped key; admin may delete/restore; scoped keys 401, non-owners 404 (404 unless SKILLX_RELEASES_API on) |
/api/v1/publishers |
api.v1.publishers.ts (POST provision an active publisher; idempotent, audited) | Admin X-Admin-Secret only; accounts 403 (404 unless SKILLX_RELEASES_API on) |
/api/v1/keys |
api.v1.signing-keys.ts | None (404 unless SKILLX_RELEASES_API on) |
/api/v1/packages/:scope/:slug/releases/:version/promote |
api.v1.release-promote.ts (POST channel move, audited; publish credentials may only promote) | Scoped key / OIDC publish token, owner session/key, or admin (404 unless SKILLX_RELEASES_API on) |
/api/v1/packages/:scope/:slug/releases/:version/review, /api/v1/admin/quarantine |
api.v1.release-review.ts, api.v1.admin-quarantine.ts (approve/reject quarantined releases; see docs/release-resolution-policy.md) | Admin X-Admin-Secret only (404 unless SKILLX_RELEASES_API on) |
/api/v1/publishers/:handle/trusted-publishers, .../:id/revoke |
api.v1.trusted-publishers.ts (GitHub OIDC trust config, audited) | Admin X-Admin-Secret only (404 unless SKILLX_RELEASES_API on) |
/api/v1/oidc/github/token |
api.v1.oidc-github-token.ts (GitHub Actions JWT → 15 min sxo_ token scoped releases:publish:@handle; jti replay-checked) |
GitHub OIDC JWT (503 without SKILLX_TOKEN_SECRET; 404 unless SKILLX_RELEASES_API on) |
/api/v1/releases/:digest/artifact/url, /api/v1/downloads/:token |
api.v1.release-download-url.ts, api.v1.release-download.ts (5 min signed artifact URL for private/entitled releases) | URL: entitled Key/Session; download: the token (404 unless SKILLX_RELEASES_API on) |
/api/v1/kits/:scope/:name, /api/v1/kits/:scope/:name/* |
api.v1.kits.ts (personal kit revisions, rebase, conflicts, session-derived proposals and review, activate/rollback, lock, soft delete/restore (admin too); see docs/personal-kits-api.md) |
Owner session or unscoped API key; scoped keys 401, non-owners 404; revisions/:id/quarantine admin secret only (404 unless SKILLX_PERSONAL_KITS_API on) |
/api/v1/collections, /api/v1/collections/:id, /api/v1/collections/:id/* |
api.v1.collections.ts (collections: create/update/delete, immutable revisions, members, clone; per-account cap, default 100; :id/moderation hides/unhides, audited; see docs/api-reference.md#collections) |
Reads: owner, or anyone for unlisted/public and not hidden; writes: owner session or unscoped key; moderation: admin X-Admin-Secret only (404 unless SKILLX_COLLECTIONS_API on) |
skills, skill_references, ratings, reviews, favorites, votes, usageStats, apiKeys, installs
Registry: packages (visibility public|unlisted|private, soft-delete deleted_at), package_releases, publishers, append-only package_audit (owner visibility/delete/restore), and more in lib/db/registry-schema.ts
Plus Better Auth tables: user, session, account, verification
System architecture: ./docs/system-architecture.md
Search algorithm: ./docs/search-algorithm.md
Page routes: Loader (server-side data) + Component (React) + Action (form handling)
API routes: Auth check → Validate input → DB operation → JSON response
Auth: getSession(request, env) for session, requireAuth(request, env) for redirect
Search: Query → Embed (Workers AI) → Vectorize + FTS5 parallel → RRF fusion → 8-signal Boost scoring (vector 50%, FTS5 21.5%, rating 3%, installs 2%, votes 0.7%, recency 1%, reviews 0.15%, favorites 0.15%) → Filter (uncached; every request recomputes)
Leaderboard: 7-signal composite scoring (rating 30%, installs 20%, votes 10%, rating_count 15%, recency 15%, reviews 5%, verified 5%). Sort tabs (best/rating/installs/trending/newest), category + risk_label filter, preview modal. Client-side interaction overlay (votes + favorites) on KV-cached data.
Skill Detail API: Returns skill metadata + references array (title, url, type) + scripts array (name, description, language, url). References indexed in Vectorize for search.
References & Scripts: Stored separately — skill_references table for external docs/links/examples (title, filename, url, type enum, content). Scripts stored as JSON in skills.scripts (name, description, language, url). CLI flags: skillx use --include-refs --include-scripts to display.
Vote API: POST /api/skills/:slug/vote with { type: 'up'|'down'|'none' } (none clears the vote). Rate limited 10 votes/min per user. Atomic count update via SQL subquery. Returns { vote_type, net_votes }.
CLI skillx use resolution: author/skill (two-part → DB slug author-skill) | org/repo/skill (three-part → DB slug org-skill, fallback register from GitHub) | slug (direct lookup, fallback search) | "keywords" (search mode). Flags: --include-refs, --include-scripts for extended content. Display logic split into use-display.ts module.
Register & Publish APIs: POST /api/skills/register with { owner, repo, skill_path?, scan? }. GitHub ownership verified via collaborator check. Content scanned for security (risk_label: safe/caution/danger/unknown). CLI skillx import owner/repo [--path X] [--scan] [--dry-run] — requires API key auth (skillx publish owner/repo is a deprecated alias). Releases: skillx validate|pack|publish <dir>|promote|visibility get|set (skillx release pack|publish are aliases), JSON envelope via --json.
Styling: Always dark theme. Use bg-slate-900, text-white, text-mint, border-mint/20. Geist Sans/Mono fonts. Lucide icons.
- Vectorize unavailable locally — search falls back to FTS5-only in local dev. Use
--remoteflag or deploy for vector search. - Auth route must be
api/auth/*notauth/*— Better Auth client SDK calls/api/auth/paths. - API keys hashed with SHA-256 — never store or log plaintext keys.
- Env vars in
apps/web/.dev.vars(local) or Cloudflare Secrets (production). Never commit.dev.vars. - Max 200 LOC per file — split into focused modules if exceeded.
- Seed data in
scripts/seed-data.json(30 real skills from skills.sh). Seed viaADMIN_SECRET=... pnpm seed. - Scoped API keys —
api_keys.scopesNULL = legacy key; scoped keys work only where their scope is checked:releases:publish:@handleon release publish/promote,library:readon release reads and kit reads,kits:write:@handleon that handle's kits (lib/auth/scoped-viewer.ts). They are stored underhashScopedApiKey(domain-separated), so legacy routes (authenticateApiKey, by plain key hash) never accept them. Apply migration 0012 to remote D1 before deploying (docs/deployment-guide.md). - AgentKit identity (
app/lib/agentkit/, flagSKILLX_AGENTKIT_IDENTITY, migration 0021) — an agentkit.bestat+jwtforhttps://skillx.sh/apiis accepted as Bearer wherever scoped keys are (resolveRequestCredential), verified locally via JWKS and mappedskillx-kits:read→library:read,skillx-kits:write→kits:write:@<active handle>. No token-exchange endpoint. Liveness: a link older than 60 s is re-validated by refreshing SkillX's own grant (one-time refresh claimed in D1);invalid_grantdeletes the link. GitHub login is unchanged; unlinked accounts behave as before. Seedocs/api-reference.md#agentkit-access-token-flag-skillx_agentkit_identity. - Releases are byte-exact — the release publish API (
app/lib/releases/) never sanitizes; the scan only pickspublishedvsquarantined. R2 writes are create-only, the D1 row is written last. Only@agentkit/*kit releases (metadataprovenance.provider = "kit") are signed asskillx.release/v2; everything else stays v1, and a registry signature never yieldssigned-kittrust. Kit release slugs omitak-(@agentkit/ak-*is 400; AgentKit mapsak-<x>to@agentkit/<x>), and clients need skillx-sh 0.7.0+ to verify v2. Seedocs/api-reference.md#publishing-releases. - Same-origin on cookie mutations —
SameSite=Laxalone lets a sibling subdomain ride the session cookie, so a mutation a session can authenticate must refuse a foreign origin withcrossOriginRefusal(app/lib/auth/same-origin.ts) before it writes. Session-only routes (rate, review, favorite, vote, user API keys) check first; routes that also take a Bearer key check only on the session path (register afterauthenticateRequest; everyresolveLimitedActorcaller — release state, promote, review, collections, personal kits, owner package/kit management —, 403cross_origin). Add the check to any new cookie-authenticated mutation. - Claude plugin —
plugins/skillx/is the only folder submitted to the Claude directory; keep everything it loads inside it. Run any CLI asskillxor an exactnpx --yes skillx-sh@<version>pin, nevernpx skillx(a different npm package). Its skill must not trigger on unrelated tasks or load/install a catalog skill without the user's explicit yes. Before changing it, runclaude plugin validate plugins/skillx --profile directory --strictand its eval suite (docs/claude-plugin.md). - Content security scanning — all SKILL.md content scanned for prompt injection, invisible chars, ANSI escapes, shell injection.
risk_labelcolumn stores result ("safe"/"caution"/"danger"/"unknown"). Sanitization strips zero-width Unicode + ANSI escapes before storage.
Read here: ./docs/deployment-guide.md
| Binding | Service |
|---|---|
DB |
D1 Database |
VECTORIZE |
Vectorize Index |
KV |
KV Namespace |
R2 |
R2 Bucket |
AI |
Workers AI |
RELEASE_READ_LIMITER |
Workers Rate Limiting (release read API, 300/60s per caller) |
RELEASE_WRITE_LIMITER |
Workers Rate Limiting (release state API, 30/60s per caller) |
All docs in ./docs/ — see design-guidelines.md for full UI/UX spec, code-standards.md for patterns, system-architecture.md for diagrams.
Claim an issue before working on it so parallel contributors and agents do not
collide: assign yourself and move it through the labels in progress → in review
(code review running) → done (PR open) → shipped (merged to main); use blocked
with a comment naming the blocker while work cannot proceed. Search open
and closed issues first; comment on a related or duplicate issue with a reference
instead of starting parallel work. File an issue for an unrelated bug or gap you
find instead of widening the current PR silently.