From 94cdd56b8263b1f50b962907921ce970549555c7 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 6 Jun 2026 08:49:54 -0700 Subject: [PATCH] feat(plugins): surface entry-point plugins in hermes plugins list Salvaged from #40346; re-verified on main, tightened, tested. Co-authored-by: tjboudreaux --- hermes_cli/plugins_cmd.py | 45 +++++++++++++- tests/hermes_cli/test_plugins_cmd_list.py | 72 +++++++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/hermes_cli/plugins_cmd.py b/hermes_cli/plugins_cmd.py index 7d6284426ee3..6a7c39f3e4e0 100644 --- a/hermes_cli/plugins_cmd.py +++ b/hermes_cli/plugins_cmd.py @@ -10,6 +10,7 @@ rendered with Rich Markdown. Otherwise a default confirmation is shown. from __future__ import annotations import functools +import importlib.metadata import json import logging import os @@ -1015,11 +1016,11 @@ def _scan_level( def _discover_all_plugins() -> list: """Return a list of (name, version, description, source, dir_path, key) for - every plugin the loader can see — user + bundled + project. + every plugin the loader can see — user + bundled + project + entry point. Matches the ordering/dedup of ``PluginManager.discover_and_load``: - bundled first, then user, then project; user overrides bundled on - key collision. + bundled first, then user, then project, then entry points. Later sources + override earlier ones on key collision. """ seen: dict = {} # key -> (name, version, description, source, path, key) @@ -1031,9 +1032,47 @@ def _discover_all_plugins() -> list: (_plugins_dir(), "user", set()), ): _scan_level(base, source, skip, "", 0, seen) + + # Entry-point plugins (installed as Python packages; no plugin directory). + for name, version, description, path in _discover_entrypoint_plugins(): + seen[name] = (name, version, description, "entrypoint", path, name) return list(seen.values()) +def _discover_entrypoint_plugins() -> list[tuple[str, str, str, str]]: + """Return plugin entries advertised through ``hermes_agent.plugins``. + + Entry-point plugins are installed as Python packages, so they do not have a + plugin directory under ``~/.hermes/plugins``. Include package metadata here + so ``hermes plugins list`` can show and enable them. + """ + from hermes_cli.plugins import ENTRY_POINTS_GROUP + + try: + eps = importlib.metadata.entry_points() + if hasattr(eps, "select"): + group_eps = eps.select(group=ENTRY_POINTS_GROUP) + elif isinstance(eps, dict): + group_eps = eps.get(ENTRY_POINTS_GROUP, []) + else: + group_eps = [ep for ep in eps if ep.group == ENTRY_POINTS_GROUP] + except Exception as exc: + logger.debug("Entry-point plugin discovery failed: %s", exc) + return [] + + entries: list[tuple[str, str, str, str]] = [] + for ep in group_eps: + version = "" + description = "" + dist = getattr(ep, "dist", None) + metadata = getattr(dist, "metadata", None) + if metadata is not None: + version = str(getattr(dist, "version", "") or "") + description = str(metadata.get("Summary", "") or "") + entries.append((ep.name, version, description, ep.value)) + return entries + + def _plugin_status(name: str, enabled: set, disabled: set, key: str = "") -> str: """Return the user-facing activation state for a plugin name or key.""" if name in disabled or key in disabled: diff --git a/tests/hermes_cli/test_plugins_cmd_list.py b/tests/hermes_cli/test_plugins_cmd_list.py index 5e8c061dab45..98a7a5e73029 100644 --- a/tests/hermes_cli/test_plugins_cmd_list.py +++ b/tests/hermes_cli/test_plugins_cmd_list.py @@ -1,5 +1,6 @@ import argparse import json +from types import SimpleNamespace from hermes_cli import plugins_cmd @@ -86,3 +87,74 @@ def test_cmd_list_json_output(monkeypatch, capsys): "source": "git", } ] + + +def test_discover_all_plugins_includes_entrypoint_plugins(monkeypatch, tmp_path): + bundled_dir = tmp_path / "bundled" + user_dir = tmp_path / "user" + bundled_dir.mkdir() + user_dir.mkdir() + + dist = SimpleNamespace( + version="0.1.0", + metadata={"Summary": "Karpathy-style LLM Wikis for Hermes"}, + ) + entry_point = SimpleNamespace( + name="wiki", + value="adapters.hermes.cli_plugin", + group="hermes_agent.plugins", + dist=dist, + ) + + monkeypatch.setattr(plugins_cmd, "_plugins_dir", lambda: user_dir) + monkeypatch.setattr( + "hermes_cli.plugins.get_bundled_plugins_dir", + lambda: bundled_dir, + ) + monkeypatch.setattr( + plugins_cmd.importlib.metadata, + "entry_points", + lambda: [entry_point], + ) + + entries = plugins_cmd._discover_all_plugins() + + assert entries == [ + ( + "wiki", + "0.1.0", + "Karpathy-style LLM Wikis for Hermes", + "entrypoint", + "adapters.hermes.cli_plugin", + "wiki", + ) + ] + + +def test_cmd_list_json_output_includes_entrypoint_source(monkeypatch, capsys): + entries = [ + ( + "wiki", + "0.1.0", + "Karpathy-style LLM Wikis for Hermes", + "entrypoint", + "adapters.hermes.cli_plugin", + "wiki", + ) + ] + monkeypatch.setattr(plugins_cmd, "_discover_all_plugins", lambda: entries) + monkeypatch.setattr(plugins_cmd, "_get_enabled_set", lambda: {"wiki"}) + monkeypatch.setattr(plugins_cmd, "_get_disabled_set", lambda: set()) + + plugins_cmd.cmd_list(_args(json=True)) + + payload = json.loads(capsys.readouterr().out) + assert payload == [ + { + "name": "wiki", + "status": "enabled", + "version": "0.1.0", + "description": "Karpathy-style LLM Wikis for Hermes", + "source": "entrypoint", + } + ]