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

@ -3,7 +3,7 @@ from io import StringIO
import pytest
from rich.console import Console
from hermes_cli.skills_hub import do_list
from hermes_cli.skills_hub import do_check, do_list, do_update
class _DummyLockFile:
@ -68,6 +68,34 @@ def _capture(source_filter: str = "all") -> str:
return sink.getvalue()
def _capture_check(monkeypatch, results, name=None) -> str:
import tools.skills_hub as hub
sink = StringIO()
console = Console(file=sink, force_terminal=False, color_system=None)
monkeypatch.setattr(hub, "check_for_skill_updates", lambda **_kwargs: results)
do_check(name=name, console=console)
return sink.getvalue()
def _capture_update(monkeypatch, results) -> tuple[str, list[tuple[str, str, bool]]]:
import tools.skills_hub as hub
import hermes_cli.skills_hub as cli_hub
sink = StringIO()
console = Console(file=sink, force_terminal=False, color_system=None)
installs = []
monkeypatch.setattr(hub, "check_for_skill_updates", lambda **_kwargs: results)
monkeypatch.setattr(hub, "HubLockFile", lambda: type("L", (), {
"get_installed": lambda self, name: {"install_path": "category/" + name}
})())
monkeypatch.setattr(cli_hub, "do_install", lambda identifier, category="", force=False, console=None: installs.append((identifier, category, force)))
do_update(console=console)
return sink.getvalue(), installs
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@ -122,3 +150,30 @@ def test_do_list_filter_builtin(three_source_env):
assert "builtin-skill" in output
assert "hub-skill" not in output
assert "local-skill" not in output
def test_do_check_reports_available_updates(monkeypatch):
output = _capture_check(monkeypatch, [
{"name": "hub-skill", "source": "skills.sh", "status": "update_available"},
{"name": "other-skill", "source": "github", "status": "up_to_date"},
])
assert "hub-skill" in output
assert "update_available" in output
assert "up_to_date" in output
def test_do_check_handles_no_installed_updates(monkeypatch):
output = _capture_check(monkeypatch, [])
assert "No hub-installed skills to check" in output
def test_do_update_reinstalls_outdated_skills(monkeypatch):
output, installs = _capture_update(monkeypatch, [
{"name": "hub-skill", "identifier": "skills-sh/example/repo/hub-skill", "status": "update_available"},
{"name": "other-skill", "identifier": "github/example/other-skill", "status": "up_to_date"},
])
assert installs == [("skills-sh/example/repo/hub-skill", "category", True)]
assert "Updated 1 skill" in output

View file

@ -8,10 +8,15 @@ from tools.skills_hub import (
GitHubAuth,
GitHubSource,
LobeHubSource,
SkillsShSource,
WellKnownSkillSource,
SkillMeta,
SkillBundle,
HubLockFile,
TapsManager,
bundle_content_hash,
check_for_skill_updates,
create_source_router,
unified_search,
append_audit_log,
_skill_meta_to_dict,
@ -93,6 +98,387 @@ class TestTrustLevelFor:
assert result in ("trusted", "community")
# ---------------------------------------------------------------------------
# SkillsShSource
# ---------------------------------------------------------------------------
class TestSkillsShSource:
def _source(self):
auth = MagicMock(spec=GitHubAuth)
return SkillsShSource(auth=auth)
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_search_maps_skills_sh_results_to_prefixed_identifiers(self, mock_get, _mock_read_cache, _mock_write_cache):
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {
"skills": [
{
"id": "vercel-labs/agent-skills/vercel-react-best-practices",
"skillId": "vercel-react-best-practices",
"name": "vercel-react-best-practices",
"installs": 207679,
"source": "vercel-labs/agent-skills",
}
]
},
)
results = self._source().search("react", limit=5)
assert len(results) == 1
assert results[0].source == "skills.sh"
assert results[0].identifier == "skills-sh/vercel-labs/agent-skills/vercel-react-best-practices"
assert "skills.sh" in results[0].description
assert results[0].repo == "vercel-labs/agent-skills"
assert results[0].path == "vercel-react-best-practices"
assert results[0].extra["installs"] == 207679
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_empty_search_uses_featured_homepage_links(self, mock_get, _mock_read_cache, _mock_write_cache):
mock_get.return_value = MagicMock(
status_code=200,
text='''
<a href="/vercel-labs/agent-skills/vercel-react-best-practices">React</a>
<a href="/anthropics/skills/pdf">PDF</a>
<a href="/vercel-labs/agent-skills/vercel-react-best-practices">React again</a>
''',
)
results = self._source().search("", limit=10)
assert [r.identifier for r in results] == [
"skills-sh/vercel-labs/agent-skills/vercel-react-best-practices",
"skills-sh/anthropics/skills/pdf",
]
assert all(r.source == "skills.sh" for r in results)
@patch.object(GitHubSource, "fetch")
def test_fetch_delegates_to_github_source_and_relabels_bundle(self, mock_fetch):
mock_fetch.return_value = SkillBundle(
name="vercel-react-best-practices",
files={"SKILL.md": "# Test"},
source="github",
identifier="vercel-labs/agent-skills/vercel-react-best-practices",
trust_level="community",
)
bundle = self._source().fetch("skills-sh/vercel-labs/agent-skills/vercel-react-best-practices")
assert bundle is not None
assert bundle.source == "skills.sh"
assert bundle.identifier == "skills-sh/vercel-labs/agent-skills/vercel-react-best-practices"
mock_fetch.assert_called_once_with("vercel-labs/agent-skills/vercel-react-best-practices")
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
@patch.object(GitHubSource, "inspect")
def test_inspect_delegates_to_github_source_and_relabels_meta(self, mock_inspect, mock_get, _mock_read_cache, _mock_write_cache):
mock_inspect.return_value = SkillMeta(
name="vercel-react-best-practices",
description="React rules",
source="github",
identifier="vercel-labs/agent-skills/vercel-react-best-practices",
trust_level="community",
repo="vercel-labs/agent-skills",
path="vercel-react-best-practices",
)
mock_get.return_value = MagicMock(
status_code=200,
text='''
<h1>vercel-react-best-practices</h1>
<code>$ npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices</code>
<div class="prose"><h1>Vercel React Best Practices</h1><p>React rules.</p></div>
<a href="/vercel-labs/agent-skills/vercel-react-best-practices/security/socket">Socket</a> Pass
<a href="/vercel-labs/agent-skills/vercel-react-best-practices/security/snyk">Snyk</a> Pass
''',
)
meta = self._source().inspect("skills-sh/vercel-labs/agent-skills/vercel-react-best-practices")
assert meta is not None
assert meta.source == "skills.sh"
assert meta.identifier == "skills-sh/vercel-labs/agent-skills/vercel-react-best-practices"
assert meta.extra["install_command"].endswith("--skill vercel-react-best-practices")
assert meta.extra["security_audits"]["socket"] == "Pass"
mock_inspect.assert_called_once_with("vercel-labs/agent-skills/vercel-react-best-practices")
@patch.object(GitHubSource, "_list_skills_in_repo")
@patch.object(GitHubSource, "inspect")
def test_inspect_falls_back_to_repo_skill_catalog_when_slug_differs(self, mock_inspect, mock_list_skills):
resolved = SkillMeta(
name="vercel-react-best-practices",
description="React rules",
source="github",
identifier="vercel-labs/agent-skills/skills/react-best-practices",
trust_level="community",
repo="vercel-labs/agent-skills",
path="skills/react-best-practices",
)
mock_inspect.side_effect = lambda identifier: resolved if identifier == resolved.identifier else None
mock_list_skills.return_value = [resolved]
meta = self._source().inspect("skills-sh/vercel-labs/agent-skills/vercel-react-best-practices")
assert meta is not None
assert meta.identifier == "skills-sh/vercel-labs/agent-skills/vercel-react-best-practices"
assert mock_list_skills.called
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
@patch.object(GitHubSource, "_list_skills_in_repo")
@patch.object(GitHubSource, "inspect")
def test_inspect_uses_detail_page_to_resolve_alias_skill(self, mock_inspect, mock_list_skills, mock_get, _mock_read_cache, _mock_write_cache):
resolved = SkillMeta(
name="react",
description="React renderer",
source="github",
identifier="vercel-labs/json-render/skills/react",
trust_level="community",
repo="vercel-labs/json-render",
path="skills/react",
)
mock_inspect.side_effect = lambda identifier: resolved if identifier == resolved.identifier else None
mock_list_skills.return_value = [resolved]
mock_get.return_value = MagicMock(
status_code=200,
text='''
<h1>json-render-react</h1>
<code>$ npx skills add https://github.com/vercel-labs/json-render --skill json-render-react</code>
<div class="prose"><h1>@json-render/react</h1><p>React renderer.</p></div>
''',
)
meta = self._source().inspect("skills-sh/vercel-labs/json-render/json-render-react")
assert meta is not None
assert meta.identifier == "skills-sh/vercel-labs/json-render/json-render-react"
assert meta.path == "skills/react"
assert mock_get.called
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
@patch.object(GitHubSource, "_list_skills_in_repo")
@patch.object(GitHubSource, "fetch")
def test_fetch_uses_detail_page_to_resolve_alias_skill(self, mock_fetch, mock_list_skills, mock_get, _mock_read_cache, _mock_write_cache):
resolved_meta = SkillMeta(
name="react",
description="React renderer",
source="github",
identifier="vercel-labs/json-render/skills/react",
trust_level="community",
repo="vercel-labs/json-render",
path="skills/react",
)
resolved_bundle = SkillBundle(
name="react",
files={"SKILL.md": "# react"},
source="github",
identifier="vercel-labs/json-render/skills/react",
trust_level="community",
)
mock_fetch.side_effect = lambda identifier: resolved_bundle if identifier == resolved_bundle.identifier else None
mock_list_skills.return_value = [resolved_meta]
mock_get.return_value = MagicMock(
status_code=200,
text='''
<h1>json-render-react</h1>
<code>$ npx skills add https://github.com/vercel-labs/json-render --skill json-render-react</code>
<div class="prose"><h1>@json-render/react</h1><p>React renderer.</p></div>
''',
)
bundle = self._source().fetch("skills-sh/vercel-labs/json-render/json-render-react")
assert bundle is not None
assert bundle.identifier == "skills-sh/vercel-labs/json-render/json-render-react"
assert bundle.files["SKILL.md"] == "# react"
assert mock_get.called
class TestWellKnownSkillSource:
def _source(self):
return WellKnownSkillSource()
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_search_reads_index_from_well_known_url(self, mock_get, _mock_read_cache, _mock_write_cache):
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {
"skills": [
{"name": "git-workflow", "description": "Git rules", "files": ["SKILL.md"]},
{"name": "code-review", "description": "Review code", "files": ["SKILL.md", "references/checklist.md"]},
]
},
)
results = self._source().search("https://example.com/.well-known/skills/index.json", limit=10)
assert [r.identifier for r in results] == [
"well-known:https://example.com/.well-known/skills/git-workflow",
"well-known:https://example.com/.well-known/skills/code-review",
]
assert all(r.source == "well-known" for r in results)
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_search_accepts_domain_root_and_resolves_index(self, mock_get, _mock_read_cache, _mock_write_cache):
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"skills": [{"name": "git-workflow", "description": "Git rules", "files": ["SKILL.md"]}]},
)
results = self._source().search("https://example.com", limit=10)
assert len(results) == 1
called_url = mock_get.call_args.args[0]
assert called_url == "https://example.com/.well-known/skills/index.json"
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_inspect_fetches_skill_md_from_well_known_endpoint(self, mock_get, _mock_read_cache, _mock_write_cache):
def fake_get(url, *args, **kwargs):
if url.endswith("/index.json"):
return MagicMock(status_code=200, json=lambda: {
"skills": [{"name": "git-workflow", "description": "Git rules", "files": ["SKILL.md"]}]
})
if url.endswith("/git-workflow/SKILL.md"):
return MagicMock(status_code=200, text="---\nname: git-workflow\ndescription: Git rules\n---\n\n# Git Workflow\n")
raise AssertionError(url)
mock_get.side_effect = fake_get
meta = self._source().inspect("well-known:https://example.com/.well-known/skills/git-workflow")
assert meta is not None
assert meta.name == "git-workflow"
assert meta.source == "well-known"
assert meta.extra["base_url"] == "https://example.com/.well-known/skills"
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_fetch_downloads_skill_files_from_well_known_endpoint(self, mock_get, _mock_read_cache, _mock_write_cache):
def fake_get(url, *args, **kwargs):
if url.endswith("/index.json"):
return MagicMock(status_code=200, json=lambda: {
"skills": [{
"name": "code-review",
"description": "Review code",
"files": ["SKILL.md", "references/checklist.md"],
}]
})
if url.endswith("/code-review/SKILL.md"):
return MagicMock(status_code=200, text="# Code Review\n")
if url.endswith("/code-review/references/checklist.md"):
return MagicMock(status_code=200, text="- [ ] security\n")
raise AssertionError(url)
mock_get.side_effect = fake_get
bundle = self._source().fetch("well-known:https://example.com/.well-known/skills/code-review")
assert bundle is not None
assert bundle.source == "well-known"
assert bundle.files["SKILL.md"] == "# Code Review\n"
assert bundle.files["references/checklist.md"] == "- [ ] security\n"
class TestCheckForSkillUpdates:
def test_bundle_content_hash_matches_installed_content_hash(self, tmp_path):
from tools.skills_guard import content_hash
bundle = SkillBundle(
name="demo-skill",
files={
"SKILL.md": "same content",
"references/checklist.md": "- [ ] security\n",
},
source="github",
identifier="owner/repo/demo-skill",
trust_level="community",
)
skill_dir = tmp_path / "demo-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("same content")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "checklist.md").write_text("- [ ] security\n")
assert bundle_content_hash(bundle) == content_hash(skill_dir)
def test_reports_update_when_remote_hash_differs(self):
lock = MagicMock()
lock.list_installed.return_value = [{
"name": "demo-skill",
"source": "github",
"identifier": "owner/repo/demo-skill",
"content_hash": "oldhash",
"install_path": "demo-skill",
}]
source = MagicMock()
source.source_id.return_value = "github"
source.fetch.return_value = SkillBundle(
name="demo-skill",
files={"SKILL.md": "new content"},
source="github",
identifier="owner/repo/demo-skill",
trust_level="community",
)
results = check_for_skill_updates(lock=lock, sources=[source])
assert len(results) == 1
assert results[0]["name"] == "demo-skill"
assert results[0]["status"] == "update_available"
def test_reports_up_to_date_when_hash_matches(self):
bundle = SkillBundle(
name="demo-skill",
files={"SKILL.md": "same content"},
source="github",
identifier="owner/repo/demo-skill",
trust_level="community",
)
lock = MagicMock()
lock.list_installed.return_value = [{
"name": "demo-skill",
"source": "github",
"identifier": "owner/repo/demo-skill",
"content_hash": bundle_content_hash(bundle),
"install_path": "demo-skill",
}]
source = MagicMock()
source.source_id.return_value = "github"
source.fetch.return_value = bundle
results = check_for_skill_updates(lock=lock, sources=[source])
assert results[0]["status"] == "up_to_date"
class TestCreateSourceRouter:
def test_includes_skills_sh_source(self):
sources = create_source_router(auth=MagicMock(spec=GitHubAuth))
assert any(isinstance(src, SkillsShSource) for src in sources)
def test_includes_well_known_source(self):
sources = create_source_router(auth=MagicMock(spec=GitHubAuth))
assert any(isinstance(src, WellKnownSkillSource) for src in sources)
# ---------------------------------------------------------------------------
# HubLockFile
# ---------------------------------------------------------------------------