diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index e18826c517b..823496157a9 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -50,20 +50,23 @@ jobs: - name: Install PyYAML for skill extraction run: pip install pyyaml==6.0.2 httpx==0.28.1 + - name: Build skills index (unified multi-source catalog) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Always rebuild — the file isn't committed (gitignored), so a + # fresh checkout starts without it and we want the freshest crawl + # in every deploy. Failure is non-fatal: extract-skills.py will + # fall back to the legacy snapshot cache and the Skills Hub page + # still renders, just without the latest community catalog. + python3 scripts/build_skills_index.py || echo "Skills index build failed (non-fatal)" + - name: Extract skill metadata for dashboard run: python3 website/scripts/extract-skills.py - name: Regenerate per-skill docs pages + catalogs run: python3 website/scripts/generate-skill-docs.py - - name: Build skills index (if not already present) - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if [ ! -f website/static/api/skills-index.json ]; then - python3 scripts/build_skills_index.py || echo "Skills index build failed (non-fatal)" - fi - - name: Install dependencies run: npm ci working-directory: website diff --git a/.github/workflows/skills-index.yml b/.github/workflows/skills-index.yml index 6d43a682495..72f252b26eb 100644 --- a/.github/workflows/skills-index.yml +++ b/.github/workflows/skills-index.yml @@ -13,6 +13,7 @@ on: permissions: contents: read + actions: write # to trigger deploy-site.yml on schedule jobs: build-index: @@ -41,61 +42,15 @@ jobs: path: website/static/api/skills-index.json retention-days: 7 - deploy-with-index: + # Re-trigger the docs deploy so the refreshed index lands on the live site. + # The deploy itself is owned by deploy-site.yml (which crawls and deploys + # everything in one pipeline); we just kick it on a schedule. + trigger-deploy: needs: build-index - runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deploy.outputs.page_url }} - # Only deploy on schedule or manual trigger (not on every push to the script) if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: skills-index - path: website/static/api/ - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: 20 - cache: npm - cache-dependency-path: website/package-lock.json - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.11' - - - name: Install PyYAML for skill extraction - run: pip install pyyaml==6.0.2 - - - name: Extract skill metadata for dashboard - run: python3 website/scripts/extract-skills.py - - - name: Install dependencies - run: npm ci - working-directory: website - - - name: Build Docusaurus - run: npm run build - working-directory: website - - - name: Stage deployment - run: | - mkdir -p _site/docs - cp -r landingpage/* _site/ - cp -r website/build/* _site/docs/ - echo "hermes-agent.nousresearch.com" > _site/CNAME - - - name: Upload artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 - with: - path: _site - - - name: Deploy to GitHub Pages - id: deploy - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + - name: Trigger Deploy Site workflow + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-site.yml --repo ${{ github.repository }} diff --git a/scripts/build_skills_index.py b/scripts/build_skills_index.py index 206a8012436..844b29733b7 100644 --- a/scripts/build_skills_index.py +++ b/scripts/build_skills_index.py @@ -40,6 +40,7 @@ from tools.skills_hub import ( ClawHubSource, ClaudeMarketplaceSource, LobeHubSource, + BrowseShSource, SkillMeta, ) import httpx @@ -260,6 +261,7 @@ def main(): "clawhub": ClawHubSource(), "claude-marketplace": ClaudeMarketplaceSource(auth=auth), "lobehub": LobeHubSource(), + "browse-sh": BrowseShSource(), } all_skills: list[dict] = [] @@ -292,7 +294,7 @@ def main(): # Sort source_order = {"official": 0, "skills-sh": 1, "skills.sh": 1, "github": 2, "well-known": 3, "clawhub": 4, - "claude-marketplace": 5, "lobehub": 6} + "browse-sh": 5, "claude-marketplace": 6, "lobehub": 7} deduped.sort(key=lambda s: (source_order.get(s["source"], 99), s["name"])) # Build index diff --git a/tools/skills_hub.py b/tools/skills_hub.py index 1dadc99495b..9021af5222f 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -391,12 +391,15 @@ class GitHubSource(SkillSource): """Fetch skills from GitHub repos via the Contents API.""" DEFAULT_TAPS = [ - {"repo": "openai/skills", "path": "skills/"}, + # NOTE: openai/skills moved its content into skills/.curated/ (and + # skills/.system/ for system-level skills). _list_skills_in_repo + # skips directories starting with "." or "_", so we point both + # entries at the inner paths directly. + {"repo": "openai/skills", "path": "skills/.curated/"}, + {"repo": "openai/skills", "path": "skills/.system/"}, {"repo": "anthropics/skills", "path": "skills/"}, {"repo": "huggingface/skills", "path": "skills/"}, - {"repo": "VoltAgent/awesome-agent-skills", "path": "skills/"}, {"repo": "garrytan/gstack", "path": ""}, - {"repo": "MiniMax-AI/cli", "path": "skill/"}, ] def __init__(self, auth: GitHubAuth, extra_taps: Optional[List[Dict]] = None): diff --git a/website/docs/user-guide/features/skills.md b/website/docs/user-guide/features/skills.md index 63b29f8c89b..c58dbb391cd 100644 --- a/website/docs/user-guide/features/skills.md +++ b/website/docs/user-guide/features/skills.md @@ -467,7 +467,6 @@ Default taps (browsable without any setup): - [openai/skills](https://github.com/openai/skills) - [anthropics/skills](https://github.com/anthropics/skills) - [huggingface/skills](https://github.com/huggingface/skills) -- [VoltAgent/awesome-agent-skills](https://github.com/VoltAgent/awesome-agent-skills) - [garrytan/gstack](https://github.com/garrytan/gstack) - Example: diff --git a/website/scripts/extract-skills.py b/website/scripts/extract-skills.py index b508eb19872..5bdb39d4f9b 100644 --- a/website/scripts/extract-skills.py +++ b/website/scripts/extract-skills.py @@ -1,5 +1,22 @@ #!/usr/bin/env python3 -"""Extract skill metadata from SKILL.md files and index caches into JSON.""" +"""Extract skill metadata into website/src/data/skills.json for the Skills Hub page. + +Two data sources: + +1. Local SKILL.md files under ``skills/`` (built-in) and ``optional-skills/`` + (official optional). These give us full metadata — overview prose, version, + license, env vars, commands — that the unified index doesn't carry. + +2. The unified Hermes Skills Index at ``website/static/api/skills-index.json``, + built twice daily by ``scripts/build_skills_index.py`` (workflow + ``.github/workflows/skills-index.yml``). Covers skills.sh, ClawHub, browse.sh, + LobeHub, Claude Marketplace, well-known endpoints, and the GitHub taps + (openai/skills, anthropics/skills, huggingface/skills, VoltAgent, etc.). + +Legacy fallback: if the unified index is missing AND ``skills/index-cache/`` +contains pre-baked JSON dumps, we read those (preserves behaviour from before +the unified index existed). +""" import json import os @@ -12,7 +29,8 @@ LOCAL_SKILL_DIRS = [ ("skills", "built-in"), ("optional-skills", "optional"), ] -INDEX_CACHE_DIR = os.path.join(REPO_ROOT, "skills", "index-cache") +UNIFIED_INDEX_PATH = os.path.join(REPO_ROOT, "website", "static", "api", "skills-index.json") +LEGACY_INDEX_CACHE_DIR = os.path.join(REPO_ROOT, "skills", "index-cache") OUTPUT = os.path.join(REPO_ROOT, "website", "src", "data", "skills.json") CATEGORY_LABELS = { @@ -48,7 +66,37 @@ CATEGORY_LABELS = { "other": "Other", } -SOURCE_LABELS = { +# Map the source ids the unified index emits to the friendly labels the +# Skills Hub UI uses. Keep these in sync with the SOURCE_CONFIG dict in +# website/src/pages/skills/index.tsx. +UNIFIED_SOURCE_LABELS = { + "official": "official", # treated as our "optional" tier in the UI + "skills.sh": "skills.sh", + "skills-sh": "skills.sh", + "clawhub": "ClawHub", + "browse-sh": "browse.sh", + "lobehub": "LobeHub", + "claude-marketplace": "Claude Marketplace", + "well-known": "Well-Known", + "github": "GitHub", # default for non-named GitHub taps +} + +# Repo-specific labels for the unified index's "github" source. Lets us +# call out the well-known taps with their vendor name instead of a generic +# "GitHub" pill. Match is checked against the leading "owner/repo/" prefix +# of the identifier. +GITHUB_TAP_LABELS = { + "openai/skills": "OpenAI", + "anthropics/skills": "Anthropic", + "huggingface/skills": "HuggingFace", + "VoltAgent/awesome-agent-skills": "VoltAgent", + "garrytan/gstack": "gstack", + "MiniMax-AI/cli": "MiniMax", +} + +# Legacy filename -> label mapping for the deprecated skills/index-cache/ +# fallback. Used only when website/static/api/skills-index.json is absent. +LEGACY_SOURCE_LABELS = { "anthropics_skills": "Anthropic", "openai_skills": "OpenAI", "claude_marketplace": "Claude Marketplace", @@ -57,31 +105,21 @@ SOURCE_LABELS = { def _extract_overview(body: str) -> str: - """Pull the first non-heading paragraph from a SKILL.md body. - - Skips H1/H2/etc. lines so the overview is real prose, not a heading. - Strips markdown links/code-fence syntax to plain-ish text. Capped at - ~500 chars so the SkillCard panel stays a reasonable size. - """ + """Pull the first non-heading paragraph from a SKILL.md body.""" if not body: return "" paragraphs = [p.strip() for p in body.split("\n\n") if p.strip()] for p in paragraphs[:6]: - # Skip pure heading paragraphs ("# Foo", "## Foo") if p.startswith("#"): - # If a heading paragraph also has body text on later lines, take those lines = [ln for ln in p.split("\n") if ln.strip() and not ln.lstrip().startswith("#")] if lines: p = "\n".join(lines).strip() else: continue - # Skip a leading admonition fence (:::tip / :::info / etc.) if p.startswith(":::"): continue - # Skip pure code fences and frontmatter-style blocks if p.startswith("```") or p.startswith("~~~"): continue - # Trim to roughly 500 chars at a sentence boundary if len(p) > 500: cut = p[:500] last_period = cut.rfind(". ") @@ -117,6 +155,37 @@ def _docs_page_path(rel_dir: str, source_label: str) -> str: return "" +def _install_command(source: str, identifier: str, name: str) -> str: + """Build the ``hermes skills install …`` command for a unified-index entry. + + These show up in the SkillCard panel so users can copy-paste them. We try + to use the most idiomatic identifier per source. + """ + if not identifier: + return f"hermes skills install {name}" + src = source.lower() + if src in {"official", "built-in", "optional"}: + # OptionalSkillSource emits identifiers like "official/security/1password" + return f"hermes skills install {identifier}" + if src in {"skills.sh", "skills-sh"}: + # Already wrapped as "skills-sh/owner/repo/skill" by the source + return f"hermes skills install {identifier}" + if src == "clawhub": + return f"hermes skills install clawhub/{identifier}" + if src == "browse-sh": + # Identifier already includes the "browse-sh/" prefix from BrowseShSource + return f"hermes skills install {identifier}" + if src == "lobehub": + return f"hermes skills install {identifier}" + if src == "claude-marketplace": + return f"hermes skills install {identifier}" + if src == "github": + return f"hermes skills install {identifier}" + if src == "well-known": + return f"hermes skills install {identifier}" + return f"hermes skills install {identifier}" + + def extract_local_skills(): skills = [] @@ -165,7 +234,6 @@ def extract_local_skills(): if isinstance(tags, str): tags = [tags] - # Optional structured prerequisites — surfaced in the SkillCard panel prereq = fm.get("prerequisites") or {} env_vars = [] commands = [] @@ -201,17 +269,104 @@ def extract_local_skills(): return skills -def extract_cached_index_skills(): +def _label_for_github_identifier(identifier: str) -> str: + """Return a friendly source label for a unified-index 'github' entry.""" + if not identifier: + return "GitHub" + for prefix, label in GITHUB_TAP_LABELS.items(): + if identifier.startswith(prefix + "/") or identifier == prefix: + return label + return "GitHub" + + +def extract_unified_index_skills(): + """Read website/static/api/skills-index.json — the canonical multi-source index.""" + if not os.path.isfile(UNIFIED_INDEX_PATH): + return None + + try: + with open(UNIFIED_INDEX_PATH, encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"[extract-skills] Failed to read unified index: {e}") + return None + + if not isinstance(data, dict) or "skills" not in data: + return None + + out = [] + for entry in data.get("skills", []): + if not isinstance(entry, dict): + continue + source_id = (entry.get("source") or "").lower() + identifier = entry.get("identifier", "") or "" + name = entry.get("name") or identifier.split("/")[-1] or "unknown" + description = (entry.get("description") or "").split("\n")[0] + if len(description) > 280: + description = description[:277] + "…" + tags = entry.get("tags", []) or [] + if not isinstance(tags, list): + tags = [] + + # Skip official entries here — extract_local_skills() already covered + # those from optional-skills/ with full metadata (overview, version, etc.). + if source_id == "official": + continue + + # Map source id -> display label + if source_id == "github": + source_label = _label_for_github_identifier(identifier) + else: + source_label = UNIFIED_SOURCE_LABELS.get(source_id, source_id or "community") + + # Guess a category from tags so the UI's category filter has a chance. + category = _guess_category(tags) + extra = entry.get("extra", {}) or {} + + # Author hint from extras when available (skills.sh has installs; + # clawhub doesn't expose author). + author = "" + if source_id in {"skills.sh", "skills-sh"}: + repo = entry.get("repo", "") + if repo: + author = repo.split("/")[0] + + install_cmd = _install_command(source_id, identifier, name) + + out.append({ + "name": name, + "description": description, + "overview": "", + "category": category, + "categoryLabel": "", # filled in _consolidate_small_categories + "source": source_label, + "tags": tags, + "platforms": [], + "author": author, + "version": "", + "license": "", + "envVars": [], + "commands": [], + "docsPath": "", + "identifier": identifier, + "installCmd": install_cmd, + }) + + return out + + +def extract_legacy_cache_skills(): + """Read the deprecated skills/index-cache/ snapshots — fallback only.""" skills = [] - if not os.path.isdir(INDEX_CACHE_DIR): + if not os.path.isdir(LEGACY_INDEX_CACHE_DIR): return skills - for filename in os.listdir(INDEX_CACHE_DIR): + for filename in os.listdir(LEGACY_INDEX_CACHE_DIR): if not filename.endswith(".json"): continue - filepath = os.path.join(INDEX_CACHE_DIR, filename) + filepath = os.path.join(LEGACY_INDEX_CACHE_DIR, filename) try: with open(filepath, encoding="utf-8") as f: data = json.load(f) @@ -220,7 +375,7 @@ def extract_cached_index_skills(): stem = filename.replace(".json", "") source_label = "community" - for key, label in SOURCE_LABELS.items(): + for key, label in LEGACY_SOURCE_LABELS.items(): if key in stem: source_label = label break @@ -233,7 +388,7 @@ def extract_cached_index_skills(): "name": agent.get("identifier", agent.get("meta", {}).get("title", "unknown")), "description": (agent.get("meta", {}).get("description", "") or "").split("\n")[0][:200], "category": _guess_category(agent.get("meta", {}).get("tags", [])), - "categoryLabel": "", # filled below + "categoryLabel": "", "source": source_label, "tags": agent.get("meta", {}).get("tags", []), "platforms": [], @@ -298,10 +453,13 @@ def _guess_category(tags: list) -> str: if not tags: return "uncategorized" for tag in tags: + if not isinstance(tag, str): + continue cat = TAG_TO_CATEGORY.get(tag.lower()) if cat: return cat - return tags[0].lower().replace(" ", "-") + first = tags[0] if isinstance(tags[0], str) else "" + return first.lower().replace(" ", "-") if first else "uncategorized" MIN_CATEGORY_SIZE = 4 @@ -320,13 +478,30 @@ def _consolidate_small_categories(skills: list) -> list: if s["category"] in small_cats: s["category"] = "other" s["categoryLabel"] = "Other" + elif not s["categoryLabel"]: + s["categoryLabel"] = CATEGORY_LABELS.get( + s["category"], + s["category"].replace("-", " ").title() if s["category"] else "Uncategorized", + ) return skills def main(): local = extract_local_skills() - external = extract_cached_index_skills() + + unified = extract_unified_index_skills() + if unified is not None: + external = unified + external_source = "unified index" + else: + external = extract_legacy_cache_skills() + external_source = "legacy index-cache" + print( + f"[extract-skills] WARNING: unified index not found at " + f"{UNIFIED_INDEX_PATH}; falling back to {external_source}. " + f"Run `python3 scripts/build_skills_index.py` to refresh." + ) all_skills = _consolidate_small_categories(local + external) @@ -345,7 +520,13 @@ def main(): print(f"Extracted {len(all_skills)} skills to {OUTPUT}") print(f" {len(local)} local ({sum(1 for s in local if s['source'] == 'built-in')} built-in, " f"{sum(1 for s in local if s['source'] == 'optional')} optional)") - print(f" {len(external)} from external indexes") + print(f" {len(external)} from {external_source}") + + # Breakdown by source + by_source = Counter(s["source"] for s in all_skills) + print("By source:") + for src, count in by_source.most_common(): + print(f" {src}: {count}") if __name__ == "__main__": diff --git a/website/scripts/prebuild.mjs b/website/scripts/prebuild.mjs index d9a5dcdeac3..32e050bd933 100644 --- a/website/scripts/prebuild.mjs +++ b/website/scripts/prebuild.mjs @@ -8,6 +8,13 @@ // CI workflows still run the extraction explicitly, which is a no-op duplicate // but matches their historical behaviour. // +// We also try to pull a fresh copy of skills-index.json (the unified +// multi-source catalog) from the live docs site if it's not already on disk. +// That way local `npm run build` doesn't have to wait on +// scripts/build_skills_index.py crawling every skill source — which takes +// several minutes and burns GitHub API quota — but still gets the same +// 2000+ external skills the deployed site has. +// // If python3 or its deps (pyyaml) aren't available on the local machine, we // fall back to writing an empty skills.json so `npm run build` still // succeeds — the Skills Hub page just shows an empty state, and llms.txt @@ -15,7 +22,7 @@ // deploys get real data. import { spawnSync } from "node:child_process"; -import { mkdirSync, writeFileSync, existsSync } from "node:fs"; +import { mkdirSync, writeFileSync, existsSync, statSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -24,6 +31,10 @@ const websiteDir = resolve(scriptDir, ".."); const extractScript = join(scriptDir, "extract-skills.py"); const llmsScript = join(scriptDir, "generate-llms-txt.py"); const outputFile = join(websiteDir, "src", "data", "skills.json"); +const unifiedIndexFile = join(websiteDir, "static", "api", "skills-index.json"); +const UNIFIED_INDEX_URL = + "https://hermes-agent.nousresearch.com/docs/api/skills-index.json"; +const UNIFIED_INDEX_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h function writeEmptyFallback(reason) { mkdirSync(dirname(outputFile), { recursive: true }); @@ -51,6 +62,64 @@ function runPython(script, label) { return true; } +async function ensureUnifiedIndex() { + // If we have a recent copy on disk, trust it. + if (existsSync(unifiedIndexFile)) { + try { + const age = Date.now() - statSync(unifiedIndexFile).mtimeMs; + if (age < UNIFIED_INDEX_MAX_AGE_MS) { + return true; + } + console.log( + `[prebuild] skills-index.json is ${(age / 3600000).toFixed(1)}h old; ` + + `refreshing from ${UNIFIED_INDEX_URL}`, + ); + } catch { + // fall through to re-fetch + } + } + + try { + const resp = await fetch(UNIFIED_INDEX_URL, { + headers: { accept: "application/json" }, + }); + if (!resp.ok) { + console.warn( + `[prebuild] skills-index.json fetch returned HTTP ${resp.status}; ` + + `using local copy if any`, + ); + return existsSync(unifiedIndexFile); + } + const text = await resp.text(); + // Sanity check: must be valid JSON with a skills array + try { + const parsed = JSON.parse(text); + if (!parsed || !Array.isArray(parsed.skills)) { + console.warn( + "[prebuild] skills-index.json from live site has no skills array; ignoring", + ); + return existsSync(unifiedIndexFile); + } + } catch (e) { + console.warn(`[prebuild] skills-index.json from live site is not valid JSON: ${e}`); + return existsSync(unifiedIndexFile); + } + mkdirSync(dirname(unifiedIndexFile), { recursive: true }); + writeFileSync(unifiedIndexFile, text); + console.log( + `[prebuild] downloaded skills-index.json from ${UNIFIED_INDEX_URL} ` + + `(${(text.length / 1024).toFixed(0)} KB)`, + ); + return true; + } catch (e) { + console.warn(`[prebuild] skills-index.json fetch failed: ${e}`); + return existsSync(unifiedIndexFile); + } +} + +// 0) Pull unified index if we don't have a fresh one. +await ensureUnifiedIndex(); + // 1) skills.json — required for the Skills Hub page. if (!existsSync(extractScript)) { writeEmptyFallback("extract script missing"); diff --git a/website/src/pages/skills/index.tsx b/website/src/pages/skills/index.tsx index 0f01f7b683f..495fb35ca5d 100644 --- a/website/src/pages/skills/index.tsx +++ b/website/src/pages/skills/index.tsx @@ -18,6 +18,8 @@ interface Skill { envVars?: string[]; commands?: string[]; docsPath?: string; + identifier?: string; + installCmd?: string; } const allSkills: Skill[] = skills as Skill[]; @@ -95,9 +97,96 @@ const SOURCE_CONFIG: Record< border: "rgba(167, 139, 250, 0.2)", icon: "\u{25A0}", }, + "skills.sh": { + label: "skills.sh", + color: "#34d399", + bg: "rgba(52, 211, 153, 0.08)", + border: "rgba(52, 211, 153, 0.2)", + icon: "\u{2734}", + }, + ClawHub: { + label: "ClawHub", + color: "#f472b6", + bg: "rgba(244, 114, 182, 0.08)", + border: "rgba(244, 114, 182, 0.2)", + icon: "\u{2726}", + }, + "browse.sh": { + label: "browse.sh", + color: "#22d3ee", + bg: "rgba(34, 211, 238, 0.08)", + border: "rgba(34, 211, 238, 0.2)", + icon: "\u{29BF}", + }, + OpenAI: { + label: "OpenAI", + color: "#10b981", + bg: "rgba(16, 185, 129, 0.08)", + border: "rgba(16, 185, 129, 0.2)", + icon: "\u{2737}", + }, + HuggingFace: { + label: "HuggingFace", + color: "#fbbf24", + bg: "rgba(251, 191, 36, 0.08)", + border: "rgba(251, 191, 36, 0.2)", + icon: "\u{1F917}", + }, + VoltAgent: { + label: "VoltAgent", + color: "#facc15", + bg: "rgba(250, 204, 21, 0.08)", + border: "rgba(250, 204, 21, 0.2)", + icon: "\u{26A1}", + }, + GitHub: { + label: "GitHub", + color: "#94a3b8", + bg: "rgba(148, 163, 184, 0.08)", + border: "rgba(148, 163, 184, 0.2)", + icon: "\u{2756}", + }, + "Well-Known": { + label: "Well-Known", + color: "#818cf8", + bg: "rgba(129, 140, 248, 0.08)", + border: "rgba(129, 140, 248, 0.2)", + icon: "\u{2756}", + }, + gstack: { + label: "gstack", + color: "#fb923c", + bg: "rgba(251, 146, 60, 0.08)", + border: "rgba(251, 146, 60, 0.2)", + icon: "\u{2756}", + }, + MiniMax: { + label: "MiniMax", + color: "#f87171", + bg: "rgba(248, 113, 113, 0.08)", + border: "rgba(248, 113, 113, 0.2)", + icon: "\u{2756}", + }, }; -const SOURCE_ORDER = ["all", "built-in", "optional", "Anthropic", "LobeHub", "Claude Marketplace"]; +const SOURCE_ORDER = [ + "all", + "built-in", + "optional", + "Anthropic", + "OpenAI", + "HuggingFace", + "skills.sh", + "ClawHub", + "browse.sh", + "LobeHub", + "Claude Marketplace", + "VoltAgent", + "Well-Known", + "GitHub", + "gstack", + "MiniMax", +]; function highlightMatch(text: string, query: string): React.ReactNode { if (!query || !text) return text; @@ -250,7 +339,7 @@ function SkillCard({ )}
- hermes skills install {skill.name} + {skill.installCmd || `hermes skills install ${skill.name}`}
{skill.docsPath && (