mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(plugins): surface category-namespaced plugins in hermes plugins list
`_discover_all_plugins()` in plugins_cmd.py did a flat scan of the bundled and user plugin directories — only direct children with a plugin.yaml were surfaced. Category directories like `observability/`, `image_gen/`, `platforms/`, `model-providers/`, `web/`, and `video_gen/` have no plugin.yaml of their own, so their nested plugins (`observability/langfuse`, `image_gen/openai`, etc.) never appeared in `hermes plugins list` or the interactive `hermes plugins` UI — even though the runtime loader (`PluginManager._scan_directory_level`) discovers them correctly and they do load at runtime. This broke the documented promise that bundled plugins appear in `hermes plugins list` and the interactive UI before being enabled, and made it look like `observability/langfuse` didn't exist. Refactor `_discover_all_plugins()` to mirror the loader's recursion (depth cap = 2, same skip set, user overrides bundled on key collision). Return the path-derived registry key (e.g. `observability/langfuse`) as the displayed name, matching what the user passes to `hermes plugins enable …` / writes under `plugins.enabled` in config.yaml. Also clarify the plugins docs: spell out that sub-category plugins surface by their `<category>/<plugin>` key in `hermes plugins list` / interactive UI, add an `observability/langfuse` example to the command reference, and include a nested entry in the interactive-UI mock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29b1bd0e20
commit
9b82586c6b
2 changed files with 70 additions and 40 deletions
|
|
@ -708,55 +708,79 @@ def _plugin_exists(name: str) -> bool:
|
|||
|
||||
|
||||
def _discover_all_plugins() -> list:
|
||||
"""Return a list of (name, version, description, source, dir_path) for
|
||||
every plugin the loader can see — user + bundled + project.
|
||||
"""Return a list of (key, version, description, source, dir_path) for
|
||||
every plugin the loader can see — user + bundled.
|
||||
|
||||
Matches the ordering/dedup of ``PluginManager.discover_and_load``:
|
||||
bundled first, then user, then project; user overrides bundled on
|
||||
name collision.
|
||||
Mirrors :meth:`PluginManager._scan_directory_level` so category-namespaced
|
||||
plugins (``observability/langfuse``, ``image_gen/openai``) surface here
|
||||
just like flat ones (``disk-cleanup``). A subdirectory with no
|
||||
``plugin.yaml`` of its own is treated as a category and recursed into
|
||||
one level deeper (depth capped at 2, same as the loader).
|
||||
|
||||
The returned ``key`` is the path-derived registry key — the value the
|
||||
user types into ``hermes plugins enable <key>``. For category-namespaced
|
||||
plugins that's ``<category>/<dirname>``; for flat plugins it's the
|
||||
manifest's ``name`` (or the directory name if the manifest omits it).
|
||||
|
||||
User entries override bundled on key collision, matching
|
||||
``PluginManager.discover_and_load``.
|
||||
"""
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
seen: dict = {} # name -> (name, version, description, source, path)
|
||||
seen: dict = {} # key -> (key, version, description, source, path)
|
||||
|
||||
# Bundled (<repo>/plugins/<name>/), excluding memory/ and context_engine/
|
||||
from hermes_cli.plugins import get_bundled_plugins_dir
|
||||
repo_plugins = get_bundled_plugins_dir()
|
||||
for base, source in ((repo_plugins, "bundled"), (_plugins_dir(), "user")):
|
||||
def _scan(base: Path, source: str, prefix: str, depth: int) -> None:
|
||||
if not base.is_dir():
|
||||
continue
|
||||
return
|
||||
for d in sorted(base.iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
if source == "bundled" and d.name in {"memory", "context_engine"}:
|
||||
if (
|
||||
depth == 0
|
||||
and source == "bundled"
|
||||
and d.name in {"memory", "context_engine"}
|
||||
):
|
||||
continue
|
||||
manifest_file = d / "plugin.yaml"
|
||||
if not manifest_file.exists():
|
||||
manifest_file = d / "plugin.yml"
|
||||
if not manifest_file.exists():
|
||||
|
||||
if manifest_file.exists():
|
||||
manifest_name = d.name
|
||||
version = ""
|
||||
description = ""
|
||||
if yaml:
|
||||
try:
|
||||
with open(manifest_file, encoding="utf-8") as f:
|
||||
manifest = yaml.safe_load(f) or {}
|
||||
manifest_name = manifest.get("name", d.name)
|
||||
version = manifest.get("version", "")
|
||||
description = manifest.get("description", "")
|
||||
except Exception:
|
||||
pass
|
||||
key = f"{prefix}/{d.name}" if prefix else manifest_name
|
||||
if key in seen and source == "bundled":
|
||||
continue
|
||||
src_label = source
|
||||
if source == "user" and (d / ".git").exists():
|
||||
src_label = "git"
|
||||
seen[key] = (key, version, description, src_label, d)
|
||||
continue
|
||||
name = d.name
|
||||
version = ""
|
||||
description = ""
|
||||
if yaml:
|
||||
try:
|
||||
with open(manifest_file, encoding="utf-8") as f:
|
||||
manifest = yaml.safe_load(f) or {}
|
||||
name = manifest.get("name", d.name)
|
||||
version = manifest.get("version", "")
|
||||
description = manifest.get("description", "")
|
||||
except Exception:
|
||||
pass
|
||||
# User plugins override bundled on name collision.
|
||||
if name in seen and source == "bundled":
|
||||
|
||||
# No manifest at this level — treat as a category namespace and
|
||||
# recurse one level deeper. Cap at depth 2 (same as the loader).
|
||||
if depth >= 1:
|
||||
continue
|
||||
src_label = source
|
||||
if source == "user" and (d / ".git").exists():
|
||||
src_label = "git"
|
||||
seen[name] = (name, version, description, src_label, d)
|
||||
sub_prefix = f"{prefix}/{d.name}" if prefix else d.name
|
||||
_scan(d, source, sub_prefix, depth + 1)
|
||||
|
||||
from hermes_cli.plugins import get_bundled_plugins_dir
|
||||
_scan(get_bundled_plugins_dir(), "bundled", "", 0)
|
||||
_scan(_plugins_dir(), "user", "", 0)
|
||||
|
||||
return list(seen.values())
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue