mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix: improve plugins list usability
This commit is contained in:
parent
c692000a57
commit
f32b66c758
3 changed files with 184 additions and 7 deletions
|
|
@ -12910,7 +12910,34 @@ Examples:
|
|||
)
|
||||
plugins_remove.add_argument("name", help="Plugin directory name to remove")
|
||||
|
||||
plugins_subparsers.add_parser("list", aliases=["ls"], help="List installed plugins")
|
||||
plugins_list = plugins_subparsers.add_parser(
|
||||
"list", aliases=["ls"], help="List installed plugins"
|
||||
)
|
||||
plugins_list.add_argument(
|
||||
"--enabled",
|
||||
action="store_true",
|
||||
help="Show only enabled plugins",
|
||||
)
|
||||
plugins_list.add_argument(
|
||||
"--user",
|
||||
action="store_true",
|
||||
help="Show only user-installed plugins (including git plugins)",
|
||||
)
|
||||
plugins_list.add_argument(
|
||||
"--no-bundled",
|
||||
action="store_true",
|
||||
help="Hide bundled plugins",
|
||||
)
|
||||
plugins_list.add_argument(
|
||||
"--plain",
|
||||
action="store_true",
|
||||
help="Print compact plain-text output instead of a Rich table",
|
||||
)
|
||||
plugins_list.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print machine-readable JSON",
|
||||
)
|
||||
|
||||
plugins_enable = plugins_subparsers.add_parser(
|
||||
"enable", help="Enable a disabled plugin"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ rendered with Rich Markdown. Otherwise a default confirmation is shown.
|
|||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
|
@ -810,7 +811,29 @@ def _discover_all_plugins() -> list:
|
|||
return list(seen.values())
|
||||
|
||||
|
||||
def cmd_list() -> None:
|
||||
def _plugin_status(name: str, enabled: set, disabled: set) -> str:
|
||||
"""Return the user-facing activation state for a plugin name."""
|
||||
if name in disabled:
|
||||
return "disabled"
|
||||
if name in enabled:
|
||||
return "enabled"
|
||||
return "not enabled"
|
||||
|
||||
|
||||
def _filter_plugin_entries(entries: list, args: Any, enabled: set, disabled: set) -> list:
|
||||
"""Apply ``hermes plugins list`` CLI filters."""
|
||||
filtered = entries
|
||||
if getattr(args, "no_bundled", False) or getattr(args, "user", False):
|
||||
filtered = [entry for entry in filtered if entry[3] != "bundled"]
|
||||
if getattr(args, "enabled", False):
|
||||
filtered = [
|
||||
entry for entry in filtered
|
||||
if _plugin_status(entry[0], enabled, disabled) == "enabled"
|
||||
]
|
||||
return filtered
|
||||
|
||||
|
||||
def cmd_list(args: Any | None = None) -> None:
|
||||
"""List all plugins (bundled + user) with enabled/disabled state."""
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
|
@ -824,6 +847,31 @@ def cmd_list() -> None:
|
|||
|
||||
enabled = _get_enabled_set()
|
||||
disabled = _get_disabled_set()
|
||||
entries = _filter_plugin_entries(entries, args, enabled, disabled)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
payload = [
|
||||
{
|
||||
"name": name,
|
||||
"status": _plugin_status(name, enabled, disabled),
|
||||
"version": str(version),
|
||||
"description": description,
|
||||
"source": source,
|
||||
}
|
||||
for name, version, description, source, _dir in entries
|
||||
]
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
|
||||
if getattr(args, "plain", False):
|
||||
for name, version, _description, source, _dir in entries:
|
||||
status = _plugin_status(name, enabled, disabled)
|
||||
print(f"{status:12} {source:8} {str(version):8} {name}")
|
||||
return
|
||||
|
||||
if not entries:
|
||||
console.print("[dim]No plugins matched the selected filters.[/dim]")
|
||||
return
|
||||
|
||||
table = Table(title="Plugins", show_lines=False)
|
||||
table.add_column("Name", style="bold")
|
||||
|
|
@ -833,9 +881,10 @@ def cmd_list() -> None:
|
|||
table.add_column("Source", style="dim")
|
||||
|
||||
for name, version, description, source, _dir in entries:
|
||||
if name in disabled:
|
||||
status_name = _plugin_status(name, enabled, disabled)
|
||||
if status_name == "disabled":
|
||||
status = "[red]disabled[/red]"
|
||||
elif name in enabled:
|
||||
elif status_name == "enabled":
|
||||
status = "[green]enabled[/green]"
|
||||
else:
|
||||
status = "[yellow]not enabled[/yellow]"
|
||||
|
|
@ -844,6 +893,7 @@ def cmd_list() -> None:
|
|||
console.print()
|
||||
console.print(table)
|
||||
console.print()
|
||||
console.print("[dim]Compact view:[/dim] hermes plugins list --plain --no-bundled")
|
||||
console.print("[dim]Interactive toggle:[/dim] hermes plugins")
|
||||
console.print("[dim]Enable/disable:[/dim] hermes plugins enable/disable <name>")
|
||||
console.print("[dim]Plugins are opt-in by default — only 'enabled' plugins load.[/dim]")
|
||||
|
|
@ -1110,7 +1160,7 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
|
|||
stdscr.addnstr(0, 0, "Plugins", max_x - 1, hattr)
|
||||
stdscr.addnstr(
|
||||
1, 0,
|
||||
" \u2191\u2193 navigate SPACE toggle ENTER configure/confirm ESC done",
|
||||
" ↑↓/j/k navigate PgUp/PgDn page SPACE toggle ENTER configure/confirm ESC done",
|
||||
max_x - 1, curses.A_DIM,
|
||||
)
|
||||
except curses.error:
|
||||
|
|
@ -1150,7 +1200,9 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
|
|||
pass
|
||||
y += 1
|
||||
|
||||
for i in range(n_plugins):
|
||||
plugin_start = scroll_offset
|
||||
plugin_stop = min(n_plugins, scroll_offset + max(visible_rows, 0))
|
||||
for i in range(plugin_start, plugin_stop):
|
||||
if y >= max_y - 1:
|
||||
break
|
||||
check = "\u2713" if i in chosen else " "
|
||||
|
|
@ -1208,6 +1260,16 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
|
|||
elif key in {curses.KEY_DOWN, ord("j")}:
|
||||
if total_items > 0:
|
||||
cursor = (cursor + 1) % total_items
|
||||
elif key in {curses.KEY_NPAGE, ord("f")}:
|
||||
if total_items > 0:
|
||||
cursor = min(total_items - 1, cursor + max(1, max_y - 5))
|
||||
elif key in {curses.KEY_PPAGE, ord("b")}:
|
||||
if total_items > 0:
|
||||
cursor = max(0, cursor - max(1, max_y - 5))
|
||||
elif key == curses.KEY_HOME:
|
||||
cursor = 0
|
||||
elif key == curses.KEY_END:
|
||||
cursor = max(0, total_items - 1)
|
||||
elif key == ord(" "):
|
||||
if cursor < n_plugins:
|
||||
# Toggle general plugin
|
||||
|
|
@ -1649,7 +1711,7 @@ def plugins_command(args) -> None:
|
|||
elif action == "disable":
|
||||
cmd_disable(args.name)
|
||||
elif action in {"list", "ls"}:
|
||||
cmd_list()
|
||||
cmd_list(args)
|
||||
elif action is None:
|
||||
cmd_toggle()
|
||||
else:
|
||||
|
|
|
|||
88
tests/hermes_cli/test_plugins_cmd_list.py
Normal file
88
tests/hermes_cli/test_plugins_cmd_list.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import argparse
|
||||
import json
|
||||
|
||||
from hermes_cli import plugins_cmd
|
||||
|
||||
|
||||
def _args(**kwargs):
|
||||
defaults = {
|
||||
"enabled": False,
|
||||
"user": False,
|
||||
"no_bundled": False,
|
||||
"plain": False,
|
||||
"json": False,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return argparse.Namespace(**defaults)
|
||||
|
||||
|
||||
def test_filter_plugin_entries_enabled_only():
|
||||
entries = [
|
||||
("disk-cleanup", "2.0.0", "Bundled", "bundled", None),
|
||||
("web-search-plus", "2.2.0", "Search", "git", None),
|
||||
("old-plugin", "1.0.0", "Old", "user", None),
|
||||
]
|
||||
|
||||
filtered = plugins_cmd._filter_plugin_entries(
|
||||
entries,
|
||||
_args(enabled=True),
|
||||
enabled={"disk-cleanup", "web-search-plus"},
|
||||
disabled={"old-plugin"},
|
||||
)
|
||||
|
||||
assert [entry[0] for entry in filtered] == ["disk-cleanup", "web-search-plus"]
|
||||
|
||||
|
||||
def test_filter_plugin_entries_no_bundled():
|
||||
entries = [
|
||||
("disk-cleanup", "2.0.0", "Bundled", "bundled", None),
|
||||
("drawthings-grpc", "0.3.0", "Draw Things", "user", None),
|
||||
("web-search-plus", "2.2.0", "Search", "git", None),
|
||||
]
|
||||
|
||||
filtered = plugins_cmd._filter_plugin_entries(
|
||||
entries,
|
||||
_args(no_bundled=True),
|
||||
enabled=set(),
|
||||
disabled=set(),
|
||||
)
|
||||
|
||||
assert [entry[0] for entry in filtered] == ["drawthings-grpc", "web-search-plus"]
|
||||
|
||||
|
||||
def test_cmd_list_plain_compact_output(monkeypatch, capsys):
|
||||
entries = [
|
||||
("disk-cleanup", "2.0.0", "Bundled", "bundled", None),
|
||||
("web-search-plus", "2.2.0", "Search", "git", None),
|
||||
]
|
||||
monkeypatch.setattr(plugins_cmd, "_discover_all_plugins", lambda: entries)
|
||||
monkeypatch.setattr(plugins_cmd, "_get_enabled_set", lambda: {"web-search-plus"})
|
||||
monkeypatch.setattr(plugins_cmd, "_get_disabled_set", lambda: set())
|
||||
|
||||
plugins_cmd.cmd_list(_args(plain=True, no_bundled=True))
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "web-search-plus" in out
|
||||
assert "enabled" in out
|
||||
assert "disk-cleanup" not in out
|
||||
assert "Search" not in out # plain mode stays compact, no descriptions
|
||||
|
||||
|
||||
def test_cmd_list_json_output(monkeypatch, capsys):
|
||||
entries = [("web-search-plus", "2.2.0", "Search", "git", None)]
|
||||
monkeypatch.setattr(plugins_cmd, "_discover_all_plugins", lambda: entries)
|
||||
monkeypatch.setattr(plugins_cmd, "_get_enabled_set", lambda: {"web-search-plus"})
|
||||
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": "web-search-plus",
|
||||
"status": "enabled",
|
||||
"version": "2.2.0",
|
||||
"description": "Search",
|
||||
"source": "git",
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue