import React, { useState, useMemo, useCallback, useRef, useEffect } from "react"; import Layout from "@theme/Layout"; import styles from "./styles.module.css"; interface Skill { name: string; description: string; overview?: string; category: string; categoryLabel: string; source: string; tags: string[]; platforms: string[]; author: string; version: string; license?: string; envVars?: string[]; commands?: string[]; docsPath?: string; identifier?: string; installCmd?: string; /** Clickable URL to the skill's origin (repo / detail page). Synthesized * in extract-skills.py for community skills that have no generated docs * page, so the expanded card always has somewhere to send the user. */ sourceUrl?: string; /** Lowercase pre-joined haystack used by the search filter. * Built once at load time so per-keystroke filtering is a single * `.includes()` per skill instead of array-join + toLowerCase on * every render. Skipped on the wire — added in the loader. */ _search?: string; } const allSkills: Skill[] = []; interface IndexMeta { extractedAt?: string; indexGeneratedAt?: string; totalSkills?: number; externalSource?: string; bySource?: Record; } const indexMeta: IndexMeta = {}; function formatRelativeTime(iso?: string): string | null { if (!iso) return null; const then = new Date(iso).getTime(); if (!Number.isFinite(then)) return null; const now = Date.now(); const diffMs = now - then; if (diffMs < 0) return "just now"; const mins = Math.floor(diffMs / 60_000); if (mins < 1) return "just now"; if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"} ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days} day${days === 1 ? "" : "s"} ago`; const months = Math.floor(days / 30); return `${months} month${months === 1 ? "" : "s"} ago`; } const CATEGORY_ICONS: Record = { apple: "\u{f179}", "autonomous-ai-agents": "\u{1F916}", blockchain: "\u{26D3}", communication: "\u{1F4AC}", creative: "\u{1F3A8}", "data-science": "\u{1F4CA}", devops: "\u{2699}", dogfood: "\u{1F436}", domain: "\u{1F310}", email: "\u{2709}", feeds: "\u{1F4E1}", gaming: "\u{1F3AE}", gifs: "\u{1F3AC}", github: "\u{1F4BB}", health: "\u{2764}", "inference-sh": "\u{26A1}", leisure: "\u{2615}", mcp: "\u{1F50C}", media: "\u{1F3B5}", migration: "\u{1F4E6}", mlops: "\u{1F9EA}", "note-taking": "\u{1F4DD}", productivity: "\u{2705}", "red-teaming": "\u{1F6E1}", research: "\u{1F50D}", security: "\u{1F512}", "smart-home": "\u{1F3E0}", "social-media": "\u{1F4F1}", "software-development": "\u{1F4BB}", translation: "\u{1F30D}", other: "\u{1F4E6}", }; const SOURCE_CONFIG: Record< string, { label: string; color: string; bg: string; border: string; icon: string } > = { "built-in": { label: "Built-in", color: "#4ade80", bg: "rgba(74, 222, 128, 0.08)", border: "rgba(74, 222, 128, 0.2)", icon: "\u{2713}", }, optional: { label: "Optional", color: "#fbbf24", bg: "rgba(251, 191, 36, 0.08)", border: "rgba(251, 191, 36, 0.2)", icon: "\u{2B50}", }, Anthropic: { label: "Anthropic", color: "#d4845a", bg: "rgba(212, 132, 90, 0.08)", border: "rgba(212, 132, 90, 0.2)", icon: "\u{25C6}", }, LobeHub: { label: "LobeHub", color: "#60a5fa", bg: "rgba(96, 165, 250, 0.08)", border: "rgba(96, 165, 250, 0.2)", icon: "\u{25CB}", }, "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}", }, NVIDIA: { label: "NVIDIA", color: "#76b900", bg: "rgba(118, 185, 0, 0.08)", border: "rgba(118, 185, 0, 0.25)", icon: "\u{25B6}", }, 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", "OpenAI", "HuggingFace", "NVIDIA", "skills.sh", "ClawHub", "browse.sh", "LobeHub", "VoltAgent", "Well-Known", "GitHub", "gstack", "MiniMax", ]; function highlightMatch(text: string, query: string): React.ReactNode { if (!query || !text) return text; const idx = text.toLowerCase().indexOf(query.toLowerCase()); if (idx === -1) return text; return ( <> {text.slice(0, idx)} {text.slice(idx, idx + query.length)} {text.slice(idx + query.length)} ); } function CopyButton({ text }: { text: string }) { const [copied, setCopied] = useState(false); const onCopy = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); navigator.clipboard?.writeText(text).then( () => { setCopied(true); setTimeout(() => setCopied(false), 1500); }, () => {}, ); }, [text], ); return ( ); } function SkillCard({ skill, query, expanded, onToggle, onCategoryClick, onTagClick, style, onPick, }: { skill: Skill; query: string; expanded: boolean; onToggle: () => void; onCategoryClick: (cat: string) => void; onTagClick: (tag: string) => void; style?: React.CSSProperties; /** Picker embed mode: render "+ Add to this Agent" and call this. */ onPick?: (skill: Skill) => void; }) { const src = SOURCE_CONFIG[skill.source] || SOURCE_CONFIG["optional"]; const icon = CATEGORY_ICONS[skill.category] || "\u{1F4E6}"; return (
{icon}

{highlightMatch(skill.name, query)}

{src.icon} {src.label}

{highlightMatch(skill.description || "No description available.", query)}

{skill.platforms?.map((p) => ( {p === "macos" ? "\u{F8FF} macOS" : p === "linux" ? "\u{1F427} Linux" : p} ))}
{expanded && (
{skill.overview && (
Overview

{skill.overview}

)} {(skill.envVars?.length || skill.commands?.length) ? (
Prerequisites {skill.envVars?.length ? (
env {skill.envVars.map((v) => ( {v} ))}
) : null} {skill.commands?.length ? (
cmd {skill.commands.map((c) => ( {c} ))}
) : null}
) : null} {skill.tags?.length > 0 && (
{skill.tags.map((tag) => ( ))}
)} {skill.author && (
Author {skill.author}
)} {skill.version && (
Version {skill.version}
)} {skill.license && (
License {skill.license}
)}
{skill.installCmd || `hermes skills install ${skill.name}`}
{onPick ? ( ) : null}
)}
); } function StatCard({ value, label, color }: { value: number; label: string; color: string }) { return (
{value} {label}
); } const PAGE_SIZE = 60; // Routes Docusaurus serves the static API JSON from. `baseUrl` is `/docs/`, // `static/api/` ends up at `/docs/api/`. Hardcoding here is fine because the // same `baseUrl` is enforced repo-wide; if it ever changes, this is the only // place that needs to follow. const SKILLS_URL = "/docs/api/skills.json"; const META_URL = "/docs/api/skills-meta.json"; function buildSearchHaystack(s: Skill): string { // Pre-compute the lowercase blob the search filter scans. Done once at // load time instead of per-keystroke per-skill. With 50k+ skills the // per-keystroke variant was unusably slow. return [ s.name, s.description, s.overview, s.categoryLabel, s.author, ...(s.tags || []), ] .filter(Boolean) .join(" ") .toLowerCase(); } export default function SkillsDashboard() { // Picker embed mode (?embed=picker): the page is being iframed by a host // app (Hermes desktop's Bot Mode agent editor) as a skill PICKER. Site // chrome is hidden via a CSS class and every card gains an // "+ Add to this Agent" button that posts // { type: 'hermes-skill-pick', name, identifier, installCmd, source } // to the parent window. The HOST performs the actual install through its // own gateway (skills.manage) — the page never installs anything, so // there is no origin to trust in this direction; parents must validate // event.origin themselves before acting on the message. const pickerMode = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("embed") === "picker"; const pickSkill = useCallback( (skill: Skill) => { if (typeof window === "undefined" || window.parent === window) return; window.parent.postMessage( { type: "hermes-skill-pick", name: skill.name, identifier: skill.identifier || skill.name, installCmd: skill.installCmd || `hermes skills install ${skill.name}`, source: skill.source, }, "*" ); }, [] ); // Lazy-loaded data. Was bundled into the JS chunk (~22 MB at 50k skills, // which made the initial page load unusable on mobile). Now fetched on // mount from the same CDN that serves the docs. const [data, setData] = useState<{ skills: Skill[]; meta: IndexMeta } | null>(null); const [loadError, setLoadError] = useState(null); const [search, setSearch] = useState(""); // Debounced copy of `search` — used by the filter. Without the debounce, // typing into the search box ran .filter() over the whole catalog on // every keystroke, which on a 50k-item list felt like the page had // hung. 150ms gives a snappy feel without lagging behind the user. const [debouncedSearch, setDebouncedSearch] = useState(""); const [sourceFilter, setSourceFilter] = useState("all"); const [categoryFilter, setCategoryFilter] = useState("all"); const [expandedCard, setExpandedCard] = useState(null); const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [sidebarOpen, setSidebarOpen] = useState(false); const searchRef = useRef(null); const gridRef = useRef(null); useEffect(() => { let cancelled = false; (async () => { try { const [sk, mt] = await Promise.all([ fetch(SKILLS_URL).then((r) => { if (!r.ok) throw new Error(`skills.json HTTP ${r.status}`); return r.json(); }), fetch(META_URL).then((r) => (r.ok ? r.json() : {})).catch(() => ({})), ]); if (cancelled) return; const skillsArr = Array.isArray(sk) ? (sk as Skill[]) : []; // Stamp the precomputed search haystack onto each row. for (const s of skillsArr) s._search = buildSearchHaystack(s); setData({ skills: skillsArr, meta: mt || {} }); } catch (err) { if (cancelled) return; setLoadError(err instanceof Error ? err.message : String(err)); } })(); return () => { cancelled = true; }; }, []); // Debounce the search input — 150ms feels instant while preventing the // filter from running on every individual keystroke. useEffect(() => { const t = setTimeout(() => setDebouncedSearch(search), 150); return () => clearTimeout(t); }, [search]); const allSkillsLocal: Skill[] = data?.skills ?? []; const indexMetaLocal: IndexMeta = data?.meta ?? indexMeta; useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "/" && document.activeElement?.tagName !== "INPUT") { e.preventDefault(); searchRef.current?.focus(); } if (e.key === "Escape") { searchRef.current?.blur(); setExpandedCard(null); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, []); const sources = useMemo(() => { const set = new Set(allSkillsLocal.map((s) => s.source)); return SOURCE_ORDER.filter((s) => s === "all" || set.has(s)); }, [allSkillsLocal]); const categoryEntries = useMemo(() => { const pool = sourceFilter === "all" ? allSkillsLocal : allSkillsLocal.filter((s) => s.source === sourceFilter); const map = new Map(); for (const s of pool) { const key = s.category || "uncategorized"; const existing = map.get(key); if (existing) { existing.count++; } else { map.set(key, { label: s.categoryLabel || s.category || "Uncategorized", count: 1, }); } } return Array.from(map.entries()) .sort((a, b) => b[1].count - a[1].count) .map(([key, { label, count }]) => ({ key, label, count })); }, [sourceFilter, allSkillsLocal]); const filtered = useMemo(() => { const q = debouncedSearch.toLowerCase().trim(); return allSkillsLocal.filter((s) => { if (sourceFilter !== "all" && s.source !== sourceFilter) return false; if (categoryFilter !== "all" && s.category !== categoryFilter) return false; if (q) { // _search is pre-built in the load effect — single .includes() per row. return (s._search || "").includes(q); } return true; }); }, [debouncedSearch, sourceFilter, categoryFilter, allSkillsLocal]); useEffect(() => { setVisibleCount(PAGE_SIZE); setExpandedCard(null); }, [debouncedSearch, sourceFilter, categoryFilter]); const visible = filtered.slice(0, visibleCount); const hasMore = visibleCount < filtered.length; const handleSourceChange = useCallback( (src: string) => { setSourceFilter(src); setCategoryFilter("all"); }, [] ); const handleCategoryClick = useCallback((cat: string) => { setCategoryFilter(cat); gridRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); setSidebarOpen(false); }, []); const handleTagClick = useCallback((tag: string) => { setSearch(tag); searchRef.current?.focus(); }, []); const clearAll = useCallback(() => { setSearch(""); setSourceFilter("all"); setCategoryFilter("all"); }, []); return (

Hermes Agent

Skills Hub

Discover, search, and install from{" "} {data ? allSkillsLocal.length.toLocaleString() : "…"} {" "} skills across {sources.length - 1} registries {loadError && ( · failed to load catalog ({loadError}) )}

{(indexMetaLocal?.indexGeneratedAt || indexMetaLocal?.extractedAt) && (

Catalog refreshed{" "} {formatRelativeTime( indexMetaLocal.indexGeneratedAt || indexMetaLocal.extractedAt, ) || "recently"} {" "}· auto-rebuilt twice daily

)}
s.source === "built-in").length} label="Built-in" color="#4ade80" /> s.source === "optional").length} label="Optional" color="#fbbf24" /> s.source !== "built-in" && s.source !== "optional" ).length } label="Community" color="#60a5fa" /> s.category)).size} label="Categories" color="#a78bfa" />
setSearch(e.target.value)} className={styles.searchInput} /> {search && ( )}
{sources.map((src) => { const active = sourceFilter === src; const conf = SOURCE_CONFIG[src]; const count = src === "all" ? allSkillsLocal.length : allSkillsLocal.filter((s) => s.source === src).length; return ( ); })}
{(search || sourceFilter !== "all" || categoryFilter !== "all") && (
{filtered.length} result{filtered.length !== 1 ? "s" : ""} {search && ( “{search}” )} {sourceFilter !== "all" && ( {SOURCE_CONFIG[sourceFilter]?.label || sourceFilter} )} {categoryFilter !== "all" && ( {categoryEntries.find((c) => c.key === categoryFilter)?.label || categoryFilter} )}
)} {!data && !loadError ? (

Loading the catalog…

Fetching 88k+ skills across every registry. One moment.

) : visible.length > 0 ? ( <>
{visible.map((skill, i) => { const key = `${skill.source}-${skill.name}-${i}`; return ( setExpandedCard(expandedCard === key ? null : key) } onCategoryClick={handleCategoryClick} onTagClick={handleTagClick} style={{ animationDelay: `${Math.min(i, 20) * 25}ms` }} onPick={pickerMode ? pickSkill : undefined} /> ); })}
{hasMore && (
)} ) : (
{"\u{1F50D}"}

No skills found

Try a different search term or clear your filters.

)}
{sidebarOpen && (
setSidebarOpen(false)} /> )} ); }