feat(plugins): add enable/disable commands + interactive toggle UI (#3747)

Adds plugin management with three interfaces:

  hermes plugins          # interactive curses checklist (like hermes tools)
  hermes plugins enable   # non-interactive enable
  hermes plugins disable  # non-interactive disable
  hermes plugins list     # table with status column

Disabled plugins are stored in config.yaml under plugins.disabled and
skipped during discovery. Uses the same curses_checklist component as
hermes tools for the interactive UI.

Changes:
- hermes_cli/plugins.py: _get_disabled_plugins() + skip disabled during
  discover_and_load()
- hermes_cli/plugins_cmd.py: cmd_toggle() interactive UI, cmd_enable(),
  cmd_disable(), updated cmd_list() with status column
- hermes_cli/main.py: enable/disable subparser entries
- website/docs/reference/cli-commands.md: updated plugins section
- website/docs/user-guide/features/plugins.md: updated managing section
This commit is contained in:
Teknium 2026-03-29 10:39:57 -07:00 committed by GitHub
parent 95f99ea4b9
commit 0df4d1278e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 210 additions and 9 deletions

View file

@ -68,6 +68,17 @@ def _env_enabled(name: str) -> bool:
return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
def _get_disabled_plugins() -> set:
"""Read the disabled plugins list from config.yaml."""
try:
from hermes_cli.config import load_config
config = load_config()
disabled = config.get("plugins", {}).get("disabled", [])
return set(disabled) if isinstance(disabled, list) else set()
except Exception:
return set()
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@ -199,8 +210,15 @@ class PluginManager:
# 3. Pip / entry-point plugins
manifests.extend(self._scan_entry_points())
# Load each manifest
# Load each manifest (skip user-disabled plugins)
disabled = _get_disabled_plugins()
for manifest in manifests:
if manifest.name in disabled:
loaded = LoadedPlugin(manifest=manifest, enabled=False)
loaded.error = "disabled via config"
self._plugins[manifest.name] = loaded
logger.debug("Skipping disabled plugin '%s'", manifest.name)
continue
self._load_plugin(manifest)
if manifests: