mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(skills): show enabled/disabled status in 'skills list' (#16129)
'hermes skills list' now shows every skill's enabled/disabled status
and accepts --enabled-only to filter down to what will actually load
for the active profile:
hermes -p dario skills list --enabled-only
Previously the command was a flat catalog — it did not apply
skills.disabled from config.yaml, so there was no way to see the
live skill set for a profile without reading config by hand.
Profile switching already works via -p (swaps HERMES_HOME); this
just surfaces the result visibly.
Changes:
- hermes_cli/skills_hub.py: do_list adds a Status column and an
enabled_only filter; summary reports enabled/disabled split
- hermes_cli/main.py: --enabled-only flag on 'skills list'
- /skills list slash command accepts --enabled-only too
- tests: 4 new (status column, disabled marking, enabled-only
hiding, no platform leakage into get_disabled_skill_names);
existing fixtures updated to accept skip_disabled kwarg
Reported by @mochizukimr on X.
This commit is contained in:
@@ -8453,6 +8453,12 @@ Examples:
|
||||
skills_list.add_argument(
|
||||
"--source", default="all", choices=["all", "hub", "builtin", "local"]
|
||||
)
|
||||
skills_list.add_argument(
|
||||
"--enabled-only",
|
||||
action="store_true",
|
||||
help="Hide disabled skills. Use with -p <profile> to see exactly "
|
||||
"which skills will load for that profile.",
|
||||
)
|
||||
|
||||
skills_check = skills_subparsers.add_parser(
|
||||
"check", help="Check installed hub skills for updates"
|
||||
|
||||
@@ -599,11 +599,24 @@ def inspect_skill(identifier: str) -> Optional[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def do_list(source_filter: str = "all", console: Optional[Console] = None) -> None:
|
||||
"""List installed skills, distinguishing hub, builtin, and local skills."""
|
||||
def do_list(source_filter: str = "all",
|
||||
enabled_only: bool = False,
|
||||
console: Optional[Console] = None) -> None:
|
||||
"""List installed skills, distinguishing hub, builtin, and local skills.
|
||||
|
||||
Args:
|
||||
source_filter: ``all`` | ``hub`` | ``builtin`` | ``local``.
|
||||
enabled_only: If True, hide disabled skills from the output.
|
||||
|
||||
Enabled/disabled state is resolved against the currently active profile's
|
||||
config — ``hermes -p <profile> skills list`` reads that profile's
|
||||
``skills.disabled`` list because ``-p`` swaps ``HERMES_HOME`` at process
|
||||
start. No explicit profile flag needed here.
|
||||
"""
|
||||
from tools.skills_hub import HubLockFile, ensure_hub_dirs
|
||||
from tools.skills_sync import _read_manifest
|
||||
from tools.skills_tool import _find_all_skills
|
||||
from agent.skill_utils import get_disabled_skill_names
|
||||
|
||||
c = console or _console
|
||||
ensure_hub_dirs()
|
||||
@@ -611,17 +624,26 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No
|
||||
hub_installed = {e["name"]: e for e in lock.list_installed()}
|
||||
builtin_names = set(_read_manifest())
|
||||
|
||||
all_skills = _find_all_skills()
|
||||
# Pull ALL skills (including disabled ones) so we can annotate status.
|
||||
all_skills = _find_all_skills(skip_disabled=True)
|
||||
disabled_names = get_disabled_skill_names()
|
||||
|
||||
table = Table(title="Installed Skills")
|
||||
title = "Installed Skills"
|
||||
if enabled_only:
|
||||
title += " (enabled only)"
|
||||
|
||||
table = Table(title=title)
|
||||
table.add_column("Name", style="bold cyan")
|
||||
table.add_column("Category", style="dim")
|
||||
table.add_column("Source", style="dim")
|
||||
table.add_column("Trust", style="dim")
|
||||
table.add_column("Status", style="dim")
|
||||
|
||||
hub_count = 0
|
||||
builtin_count = 0
|
||||
local_count = 0
|
||||
enabled_count = 0
|
||||
disabled_count = 0
|
||||
|
||||
for skill in sorted(all_skills, key=lambda s: (s.get("category") or "", s["name"])):
|
||||
name = skill["name"]
|
||||
@@ -632,29 +654,48 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No
|
||||
source_type = "hub"
|
||||
source_display = hub_entry.get("source", "hub")
|
||||
trust = hub_entry.get("trust_level", "community")
|
||||
hub_count += 1
|
||||
elif name in builtin_names:
|
||||
source_type = "builtin"
|
||||
source_display = "builtin"
|
||||
trust = "builtin"
|
||||
builtin_count += 1
|
||||
else:
|
||||
source_type = "local"
|
||||
source_display = "local"
|
||||
trust = "local"
|
||||
local_count += 1
|
||||
|
||||
if source_filter != "all" and source_filter != source_type:
|
||||
continue
|
||||
|
||||
is_enabled = name not in disabled_names
|
||||
if enabled_only and not is_enabled:
|
||||
continue
|
||||
|
||||
if source_type == "hub":
|
||||
hub_count += 1
|
||||
elif source_type == "builtin":
|
||||
builtin_count += 1
|
||||
else:
|
||||
local_count += 1
|
||||
|
||||
if is_enabled:
|
||||
enabled_count += 1
|
||||
status_cell = "[bold green]enabled[/]"
|
||||
else:
|
||||
disabled_count += 1
|
||||
status_cell = "[dim red]disabled[/]"
|
||||
|
||||
trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow", "local": "dim"}.get(trust, "dim")
|
||||
trust_label = "official" if source_display == "official" else trust
|
||||
table.add_row(name, category, source_display, f"[{trust_style}]{trust_label}[/]")
|
||||
table.add_row(name, category, source_display, f"[{trust_style}]{trust_label}[/]", status_cell)
|
||||
|
||||
c.print(table)
|
||||
c.print(
|
||||
f"[dim]{hub_count} hub-installed, {builtin_count} builtin, {local_count} local[/]\n"
|
||||
)
|
||||
summary = f"[dim]{hub_count} hub-installed, {builtin_count} builtin, {local_count} local"
|
||||
if enabled_only:
|
||||
summary += f" — {enabled_count} enabled shown"
|
||||
else:
|
||||
summary += f" — {enabled_count} enabled, {disabled_count} disabled"
|
||||
summary += "[/]\n"
|
||||
c.print(summary)
|
||||
|
||||
|
||||
def do_check(name: Optional[str] = None, console: Optional[Console] = None) -> None:
|
||||
@@ -1127,7 +1168,10 @@ def skills_command(args) -> None:
|
||||
elif action == "inspect":
|
||||
do_inspect(args.identifier)
|
||||
elif action == "list":
|
||||
do_list(source_filter=args.source)
|
||||
do_list(
|
||||
source_filter=args.source,
|
||||
enabled_only=getattr(args, "enabled_only", False),
|
||||
)
|
||||
elif action == "check":
|
||||
do_check(name=getattr(args, "name", None))
|
||||
elif action == "update":
|
||||
@@ -1279,11 +1323,12 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
|
||||
|
||||
elif action == "list":
|
||||
source_filter = "all"
|
||||
enabled_only = "--enabled-only" in args or "--enabled" in args
|
||||
if "--source" in args:
|
||||
idx = args.index("--source")
|
||||
if idx + 1 < len(args):
|
||||
source_filter = args[idx + 1]
|
||||
do_list(source_filter=source_filter, console=c)
|
||||
do_list(source_filter=source_filter, enabled_only=enabled_only, console=c)
|
||||
|
||||
elif action == "check":
|
||||
name = args[0] if args else None
|
||||
@@ -1371,7 +1416,8 @@ def _print_skills_help(console: Console) -> None:
|
||||
" [cyan]search[/] <query> Search registries for skills\n"
|
||||
" [cyan]install[/] <identifier> Install a skill (with security scan)\n"
|
||||
" [cyan]inspect[/] <identifier> Preview a skill without installing\n"
|
||||
" [cyan]list[/] [--source hub|builtin|local] List installed skills\n"
|
||||
" [cyan]list[/] [--source hub|builtin|local] [--enabled-only]\n"
|
||||
" List installed skills; --enabled-only filters to the active profile's live set\n"
|
||||
" [cyan]check[/] [name] Check hub skills for upstream updates\n"
|
||||
" [cyan]update[/] [name] Update hub skills with upstream changes\n"
|
||||
" [cyan]audit[/] [name] Re-scan hub skills for security\n"
|
||||
|
||||
@@ -56,7 +56,7 @@ def three_source_env(monkeypatch, hub_env):
|
||||
import tools.skills_tool as skills_tool
|
||||
|
||||
monkeypatch.setattr(hub, "HubLockFile", lambda: _DummyLockFile([_HUB_ENTRY]))
|
||||
monkeypatch.setattr(skills_tool, "_find_all_skills", lambda: list(_ALL_THREE_SKILLS))
|
||||
monkeypatch.setattr(skills_tool, "_find_all_skills", lambda **_kwargs: list(_ALL_THREE_SKILLS))
|
||||
monkeypatch.setattr(skills_sync, "_read_manifest", lambda: dict(_BUILTIN_MANIFEST))
|
||||
|
||||
return hub_env
|
||||
@@ -107,7 +107,7 @@ def test_do_list_initializes_hub_dir(monkeypatch, hub_env):
|
||||
import tools.skills_sync as skills_sync
|
||||
import tools.skills_tool as skills_tool
|
||||
|
||||
monkeypatch.setattr(skills_tool, "_find_all_skills", lambda: [])
|
||||
monkeypatch.setattr(skills_tool, "_find_all_skills", lambda **_kwargs: [])
|
||||
monkeypatch.setattr(skills_sync, "_read_manifest", lambda: {})
|
||||
|
||||
hub_dir = hub_env
|
||||
@@ -154,6 +154,74 @@ def test_do_list_filter_builtin(three_source_env):
|
||||
assert "local-skill" not in output
|
||||
|
||||
|
||||
def test_do_list_renders_status_column(three_source_env, monkeypatch):
|
||||
"""Every list row should carry an enabled/disabled status (new in PR that
|
||||
answered Mr Mochizuki's 'I just want to see what's live' question)."""
|
||||
from agent import skill_utils
|
||||
|
||||
monkeypatch.setattr(skill_utils, "get_disabled_skill_names", lambda platform=None: set())
|
||||
output = _capture()
|
||||
|
||||
assert "Status" in output
|
||||
assert "enabled" in output.lower()
|
||||
# Summary counts enabled skills.
|
||||
assert "3 enabled, 0 disabled" in output
|
||||
|
||||
|
||||
def test_do_list_marks_disabled_skills(three_source_env, monkeypatch):
|
||||
from agent import skill_utils
|
||||
|
||||
# Simulate `skills.disabled: [hub-skill]` in config.
|
||||
monkeypatch.setattr(
|
||||
skill_utils, "get_disabled_skill_names",
|
||||
lambda platform=None: {"hub-skill"},
|
||||
)
|
||||
output = _capture()
|
||||
|
||||
# Row still appears (no --enabled-only), but marked disabled
|
||||
assert "hub-skill" in output
|
||||
assert "disabled" in output.lower()
|
||||
assert "2 enabled, 1 disabled" in output
|
||||
|
||||
|
||||
def test_do_list_enabled_only_hides_disabled(three_source_env, monkeypatch):
|
||||
from agent import skill_utils
|
||||
|
||||
monkeypatch.setattr(
|
||||
skill_utils, "get_disabled_skill_names",
|
||||
lambda platform=None: {"hub-skill"},
|
||||
)
|
||||
sink = StringIO()
|
||||
console = Console(file=sink, force_terminal=False, color_system=None)
|
||||
do_list(enabled_only=True, console=console)
|
||||
output = sink.getvalue()
|
||||
|
||||
assert "hub-skill" not in output
|
||||
assert "builtin-skill" in output
|
||||
assert "local-skill" in output
|
||||
assert "enabled only" in output.lower()
|
||||
assert "2 enabled shown" in output
|
||||
|
||||
|
||||
def test_do_list_platform_env_is_ignored(three_source_env, monkeypatch):
|
||||
"""`hermes skills list` reads the active profile's config via
|
||||
HERMES_HOME (swapped by -p), so it must NOT pass a platform arg to
|
||||
``get_disabled_skill_names`` — otherwise per-platform overrides
|
||||
would silently leak in from HERMES_PLATFORM env."""
|
||||
from agent import skill_utils
|
||||
|
||||
seen = {}
|
||||
|
||||
def _fake(platform=None):
|
||||
seen["platform"] = platform
|
||||
return set()
|
||||
|
||||
monkeypatch.setattr(skill_utils, "get_disabled_skill_names", _fake)
|
||||
_capture()
|
||||
|
||||
assert seen["platform"] is None
|
||||
|
||||
|
||||
def test_do_check_reports_available_updates(monkeypatch):
|
||||
output = _capture_check(monkeypatch, [
|
||||
{"name": "hub-skill", "source": "skills.sh", "status": "update_available"},
|
||||
|
||||
Reference in New Issue
Block a user