Merge pull request #1303 from NousResearch/hermes/hermes-aa653753

feat(skills): integrate skills.sh as a hub source
This commit is contained in:
Teknium 2026-03-14 09:48:18 -07:00 committed by GitHub
commit 681f1068ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1413 additions and 47 deletions

View file

@ -2682,7 +2682,7 @@ For more help on a command:
skills_parser = subparsers.add_parser(
"skills",
help="Search, install, configure, and manage skills",
description="Search, install, inspect, audit, configure, and manage skills from GitHub, ClawHub, and other registries."
description="Search, install, inspect, audit, configure, and manage skills from skills.sh, well-known agent skill endpoints, GitHub, ClawHub, and other registries."
)
skills_subparsers = skills_parser.add_subparsers(dest="skills_action")
@ -2690,12 +2690,12 @@ For more help on a command:
skills_browse.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
skills_browse.add_argument("--size", type=int, default=20, help="Results per page (default: 20)")
skills_browse.add_argument("--source", default="all",
choices=["all", "official", "github", "clawhub", "lobehub"],
choices=["all", "official", "skills-sh", "well-known", "github", "clawhub", "lobehub"],
help="Filter by source (default: all)")
skills_search = skills_subparsers.add_parser("search", help="Search skill registries")
skills_search.add_argument("query", help="Search query")
skills_search.add_argument("--source", default="all", choices=["all", "official", "github", "clawhub", "lobehub"])
skills_search.add_argument("--source", default="all", choices=["all", "official", "skills-sh", "well-known", "github", "clawhub", "lobehub"])
skills_search.add_argument("--limit", type=int, default=10, help="Max results")
skills_install = skills_subparsers.add_parser("install", help="Install a skill")
@ -2709,6 +2709,12 @@ For more help on a command:
skills_list = skills_subparsers.add_parser("list", help="List installed skills")
skills_list.add_argument("--source", default="all", choices=["all", "hub", "builtin", "local"])
skills_check = skills_subparsers.add_parser("check", help="Check installed hub skills for updates")
skills_check.add_argument("name", nargs="?", help="Specific skill to check (default: all)")
skills_update = skills_subparsers.add_parser("update", help="Update installed hub skills")
skills_update.add_argument("name", nargs="?", help="Specific skill to update (default: all outdated skills)")
skills_audit = skills_subparsers.add_parser("audit", help="Re-scan installed hub skills")
skills_audit.add_argument("name", nargs="?", help="Specific skill to audit (default: all)")

View file

@ -13,7 +13,7 @@ handler are thin wrappers that parse args and delegate.
import json
import shutil
from pathlib import Path
from typing import Optional
from typing import Any, Dict, Optional
from rich.console import Console
from rich.panel import Panel
@ -76,6 +76,70 @@ def _resolve_short_name(name: str, sources, console: Console) -> str:
return ""
def _format_extra_metadata_lines(extra: Dict[str, Any]) -> list[str]:
lines: list[str] = []
if not extra:
return lines
if extra.get("repo_url"):
lines.append(f"[bold]Repo:[/] {extra['repo_url']}")
if extra.get("detail_url"):
lines.append(f"[bold]Detail Page:[/] {extra['detail_url']}")
if extra.get("index_url"):
lines.append(f"[bold]Index:[/] {extra['index_url']}")
if extra.get("endpoint"):
lines.append(f"[bold]Endpoint:[/] {extra['endpoint']}")
if extra.get("install_command"):
lines.append(f"[bold]Install Command:[/] {extra['install_command']}")
if extra.get("installs") is not None:
lines.append(f"[bold]Installs:[/] {extra['installs']}")
if extra.get("weekly_installs"):
lines.append(f"[bold]Weekly Installs:[/] {extra['weekly_installs']}")
security = extra.get("security_audits")
if isinstance(security, dict) and security:
ordered = ", ".join(f"{name}={status}" for name, status in sorted(security.items()))
lines.append(f"[bold]Security:[/] {ordered}")
return lines
def _resolve_source_meta_and_bundle(identifier: str, sources):
"""Resolve metadata and bundle for a specific identifier."""
meta = None
bundle = None
matched_source = None
for src in sources:
if meta is None:
try:
meta = src.inspect(identifier)
if meta:
matched_source = src
except Exception:
meta = None
try:
bundle = src.fetch(identifier)
except Exception:
bundle = None
if bundle:
matched_source = src
if meta is None:
try:
meta = src.inspect(identifier)
except Exception:
meta = None
break
return meta, bundle, matched_source
def _derive_category_from_install_path(install_path: str) -> str:
path = Path(install_path)
parent = str(path.parent)
return "" if parent == "." else parent
def do_search(query: str, source: str = "all", limit: int = 10,
console: Optional[Console] = None) -> None:
"""Search registries and display results as a Rich table."""
@ -136,7 +200,7 @@ def do_browse(page: int = 1, page_size: int = 20, source: str = "all",
# Collect results from all (or filtered) sources
# Use empty query to get everything; per-source limits prevent overload
_TRUST_RANK = {"builtin": 3, "trusted": 2, "community": 1}
_PER_SOURCE_LIMIT = {"official": 100, "github": 100, "clawhub": 50,
_PER_SOURCE_LIMIT = {"official": 100, "skills-sh": 100, "well-known": 25, "github": 100, "clawhub": 50,
"claude-marketplace": 50, "lobehub": 50}
all_results: list = []
@ -263,11 +327,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
c.print(f"\n[bold]Fetching:[/] {identifier}")
bundle = None
for src in sources:
bundle = src.fetch(identifier)
if bundle:
break
meta, bundle, _matched_source = _resolve_source_meta_and_bundle(identifier, sources)
if not bundle:
c.print(f"[bold red]Error:[/] Could not fetch '{identifier}' from any source.\n")
@ -288,6 +348,9 @@ def do_install(identifier: str, category: str = "", force: bool = False,
c.print("Use --force to reinstall.\n")
return
extra_metadata = dict(getattr(meta, "extra", {}) or {})
extra_metadata.update(getattr(bundle, "metadata", {}) or {})
# Quarantine the bundle
q_path = quarantine_bundle(bundle)
c.print(f"[dim]Quarantined to {q_path.relative_to(q_path.parent.parent.parent)}[/]")
@ -309,6 +372,11 @@ def do_install(identifier: str, category: str = "", force: bool = False,
f"{len(result.findings)}_findings")
return
if extra_metadata:
metadata_lines = _format_extra_metadata_lines(extra_metadata)
if metadata_lines:
c.print(Panel("\n".join(metadata_lines), title="Upstream Metadata", border_style="blue"))
# Confirm with user — show appropriate warning based on source
if not force:
c.print()
@ -361,23 +429,12 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
if not identifier:
return
meta = None
for src in sources:
meta = src.inspect(identifier)
if meta:
break
meta, bundle, _matched_source = _resolve_source_meta_and_bundle(identifier, sources)
if not meta:
c.print(f"[bold red]Error:[/] Could not find '{identifier}' in any source.\n")
return
# Also fetch full content for preview
bundle = None
for src in sources:
bundle = src.fetch(identifier)
if bundle:
break
c.print()
trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(meta.trust_level, "dim")
trust_label = "official" if meta.source == "official" else meta.trust_level
@ -391,6 +448,7 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
]
if meta.tags:
info_lines.append(f"[bold]Tags:[/] {', '.join(meta.tags)}")
info_lines.extend(_format_extra_metadata_lines(meta.extra))
c.print(Panel("\n".join(info_lines), title=f"Skill: {meta.name}"))
@ -464,6 +522,49 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No
)
def do_check(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Check hub-installed skills for upstream updates."""
from tools.skills_hub import check_for_skill_updates
c = console or _console
results = check_for_skill_updates(name=name)
if not results:
c.print("[dim]No hub-installed skills to check.[/]\n")
return
table = Table(title="Skill Updates")
table.add_column("Name", style="bold cyan")
table.add_column("Source", style="dim")
table.add_column("Status", style="dim")
for entry in results:
table.add_row(entry.get("name", ""), entry.get("source", ""), entry.get("status", ""))
c.print(table)
update_count = sum(1 for entry in results if entry.get("status") == "update_available")
c.print(f"[dim]{update_count} update(s) available across {len(results)} checked skill(s)[/]\n")
def do_update(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Update hub-installed skills with upstream changes."""
from tools.skills_hub import HubLockFile, check_for_skill_updates
c = console or _console
lock = HubLockFile()
updates = [entry for entry in check_for_skill_updates(name=name) if entry.get("status") == "update_available"]
if not updates:
c.print("[dim]No updates available.[/]\n")
return
for entry in updates:
installed = lock.get_installed(entry["name"])
category = _derive_category_from_install_path(installed.get("install_path", "")) if installed else ""
c.print(f"[bold]Updating:[/] {entry['name']}")
do_install(entry["identifier"], category=category, force=True, console=c)
c.print(f"[bold green]Updated {len(updates)} skill(s).[/]\n")
def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Re-run security scan on installed hub skills."""
from tools.skills_hub import HubLockFile, SKILLS_DIR
@ -827,6 +928,10 @@ def skills_command(args) -> None:
do_inspect(args.identifier)
elif action == "list":
do_list(source_filter=args.source)
elif action == "check":
do_check(name=getattr(args, "name", None))
elif action == "update":
do_update(name=getattr(args, "name", None))
elif action == "audit":
do_audit(name=getattr(args, "name", None))
elif action == "uninstall":
@ -853,7 +958,7 @@ def skills_command(args) -> None:
return
do_tap(tap_action, repo=repo)
else:
_console.print("Usage: hermes skills [browse|search|install|inspect|list|audit|uninstall|publish|snapshot|tap]\n")
_console.print("Usage: hermes skills [browse|search|install|inspect|list|check|update|audit|uninstall|publish|snapshot|tap]\n")
_console.print("Run 'hermes skills <command> --help' for details.\n")
@ -872,6 +977,8 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
/skills inspect openai/skills/skill-creator
/skills list
/skills list --source hub
/skills check
/skills update
/skills audit
/skills audit my-skill
/skills uninstall my-skill
@ -920,7 +1027,7 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
elif action == "search":
if not args:
c.print("[bold red]Usage:[/] /skills search <query> [--source github] [--limit N]\n")
c.print("[bold red]Usage:[/] /skills search <query> [--source skills-sh|well-known|github|official] [--limit N]\n")
return
source = "all"
limit = 10
@ -967,6 +1074,14 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
source_filter = args[idx + 1]
do_list(source_filter=source_filter, console=c)
elif action == "check":
name = args[0] if args else None
do_check(name=name, console=c)
elif action == "update":
name = args[0] if args else None
do_update(name=name, console=c)
elif action == "audit":
name = args[0] if args else None
do_audit(name=name, console=c)
@ -1029,6 +1144,8 @@ def _print_skills_help(console: Console) -> None:
" [cyan]install[/] <identifier> Install a skill (with security scan)\n"
" [cyan]inspect[/] <identifier> Preview a skill without installing\n"
" [cyan]list[/] [--source hub|builtin|local] List installed skills\n"
" [cyan]check[/] [name] Check hub skills for upstream updates\n"
" [cyan]update[/] [name] Update hub skills with upstream changes\n"
" [cyan]audit[/] [name] Re-scan hub skills for security\n"
" [cyan]uninstall[/] <name> Remove a hub-installed skill\n"
" [cyan]publish[/] <path> --repo <r> Publish a skill to GitHub via PR\n"