mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 20:29:00 +08:00
Compare commits
1 Commits
main
...
hermes/cur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
762b681423 |
@@ -473,6 +473,66 @@ def _cmd_list_archived(args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_usage(args) -> int:
|
||||
"""Show usage telemetry for ALL skills, with provenance.
|
||||
|
||||
Unlike `status` (curator-scoped to agent-created candidates), this lists
|
||||
every skill on disk — bundled built-ins and hub-installed included — so you
|
||||
can see how often each is actually used regardless of curation.
|
||||
"""
|
||||
import json as _json
|
||||
from tools import skill_usage
|
||||
|
||||
rows = skill_usage.usage_report()
|
||||
|
||||
prov_filter = getattr(args, "provenance", None)
|
||||
if prov_filter:
|
||||
rows = [r for r in rows if r.get("provenance") == prov_filter]
|
||||
|
||||
sort_key = getattr(args, "sort", "activity")
|
||||
if sort_key == "name":
|
||||
rows.sort(key=lambda r: r["name"])
|
||||
elif sort_key == "recent":
|
||||
# Most-recently-active first; never-active sinks to the bottom.
|
||||
rows.sort(key=lambda r: r.get("last_activity_at") or "", reverse=True)
|
||||
else: # "activity" (default): most-used first
|
||||
rows.sort(key=lambda r: r.get("activity_count", 0), reverse=True)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
print(_json.dumps(rows, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
if not rows:
|
||||
print("curator: no skills found")
|
||||
return 0
|
||||
|
||||
# Provenance tallies for a quick header.
|
||||
counts = {"agent": 0, "bundled": 0, "hub": 0}
|
||||
for r in rows:
|
||||
counts[r.get("provenance", "agent")] = counts.get(r.get("provenance", "agent"), 0) + 1
|
||||
print(
|
||||
f"skills: {len(rows)} total "
|
||||
f"(agent={counts['agent']} bundled={counts['bundled']} hub={counts['hub']})"
|
||||
)
|
||||
print()
|
||||
print(
|
||||
f" {'skill':40s} {'origin':8s} "
|
||||
f"{'use':>4s} {'view':>4s} {'patch':>5s} {'act':>4s} last_activity"
|
||||
)
|
||||
for r in rows:
|
||||
last = _fmt_ts(r.get("last_activity_at"))
|
||||
print(
|
||||
f" {r['name'][:40]:40s} "
|
||||
f"{r.get('provenance', 'agent'):8s} "
|
||||
f"{r.get('use_count', 0):>4d} "
|
||||
f"{r.get('view_count', 0):>4d} "
|
||||
f"{r.get('patch_count', 0):>5d} "
|
||||
f"{r.get('activity_count', 0):>4d} "
|
||||
f"{last}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# argparse wiring (called from hermes_cli.main)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -489,6 +549,25 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
|
||||
p_status = subs.add_parser("status", help="Show curator status and skill stats")
|
||||
p_status.set_defaults(func=_cmd_status)
|
||||
|
||||
p_usage = subs.add_parser(
|
||||
"usage",
|
||||
help="Show usage telemetry for ALL skills (built-in, hub, agent) with provenance",
|
||||
)
|
||||
p_usage.add_argument(
|
||||
"--sort", choices=("activity", "recent", "name"), default="activity",
|
||||
help="Sort order: activity (most-used first, default), recent "
|
||||
"(most-recently-active first), or name (alphabetical)",
|
||||
)
|
||||
p_usage.add_argument(
|
||||
"--provenance", choices=("agent", "bundled", "hub"), default=None,
|
||||
help="Only show skills of this origin",
|
||||
)
|
||||
p_usage.add_argument(
|
||||
"--json", action="store_true",
|
||||
help="Emit the full report as JSON instead of a table",
|
||||
)
|
||||
p_usage.set_defaults(func=_cmd_usage)
|
||||
|
||||
p_run = subs.add_parser("run", help="Trigger a curator review now")
|
||||
p_run.add_argument(
|
||||
"--sync", "--synchronous", dest="synchronous", action="store_true",
|
||||
|
||||
112
tests/hermes_cli/test_curator_usage.py
Normal file
112
tests/hermes_cli/test_curator_usage.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Tests for `hermes curator usage` — the all-skills usage view.
|
||||
|
||||
Covers:
|
||||
- Lists every skill regardless of provenance (agent / bundled / hub), unlike
|
||||
`status` which is scoped to curator-managed candidates.
|
||||
- --provenance filter, --sort ordering, and --json output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def _fake_rows():
|
||||
return [
|
||||
{
|
||||
"name": "agent-skill", "provenance": "agent", "state": "active",
|
||||
"use_count": 2, "view_count": 1, "patch_count": 0,
|
||||
"activity_count": 3, "last_activity_at": "2026-05-01T10:00:00+00:00",
|
||||
"created_at": "2026-01-01T00:00:00+00:00", "_persisted": True,
|
||||
},
|
||||
{
|
||||
"name": "bundled-skill", "provenance": "bundled", "state": "active",
|
||||
"use_count": 9, "view_count": 4, "patch_count": 0,
|
||||
"activity_count": 13, "last_activity_at": "2026-05-10T10:00:00+00:00",
|
||||
"created_at": "2026-01-01T00:00:00+00:00", "_persisted": True,
|
||||
},
|
||||
{
|
||||
"name": "hub-skill", "provenance": "hub", "state": "active",
|
||||
"use_count": 0, "view_count": 0, "patch_count": 0,
|
||||
"activity_count": 0, "last_activity_at": None,
|
||||
"created_at": "2026-01-01T00:00:00+00:00", "_persisted": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_usage_lists_all_provenances(monkeypatch, capsys):
|
||||
import hermes_cli.curator as curator_cli
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
|
||||
args = SimpleNamespace(sort="activity", provenance=None, json=False)
|
||||
assert curator_cli._cmd_usage(args) == 0
|
||||
out = capsys.readouterr().out
|
||||
# Header tally and all three skills present.
|
||||
assert "agent=1" in out and "bundled=1" in out and "hub=1" in out
|
||||
assert "agent-skill" in out
|
||||
assert "bundled-skill" in out
|
||||
assert "hub-skill" in out
|
||||
|
||||
|
||||
def test_usage_sort_activity_orders_most_used_first(monkeypatch, capsys):
|
||||
import hermes_cli.curator as curator_cli
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
|
||||
args = SimpleNamespace(sort="activity", provenance=None, json=False)
|
||||
assert curator_cli._cmd_usage(args) == 0
|
||||
out = capsys.readouterr().out
|
||||
# bundled-skill (act=13) must appear before agent-skill (act=3).
|
||||
assert out.index("bundled-skill") < out.index("agent-skill")
|
||||
|
||||
|
||||
def test_usage_provenance_filter(monkeypatch, capsys):
|
||||
import hermes_cli.curator as curator_cli
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
|
||||
args = SimpleNamespace(sort="activity", provenance="bundled", json=False)
|
||||
assert curator_cli._cmd_usage(args) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "bundled-skill" in out
|
||||
assert "agent-skill" not in out
|
||||
assert "hub-skill" not in out
|
||||
|
||||
|
||||
def test_usage_json_output(monkeypatch, capsys):
|
||||
import hermes_cli.curator as curator_cli
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
|
||||
args = SimpleNamespace(sort="name", provenance=None, json=True)
|
||||
assert curator_cli._cmd_usage(args) == 0
|
||||
out = capsys.readouterr().out
|
||||
data = json.loads(out)
|
||||
assert {r["name"] for r in data} == {"agent-skill", "bundled-skill", "hub-skill"}
|
||||
assert {r["provenance"] for r in data} == {"agent", "bundled", "hub"}
|
||||
|
||||
|
||||
def test_usage_empty(monkeypatch, capsys):
|
||||
import hermes_cli.curator as curator_cli
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
monkeypatch.setattr(skill_usage, "usage_report", lambda: [])
|
||||
args = SimpleNamespace(sort="activity", provenance=None, json=False)
|
||||
assert curator_cli._cmd_usage(args) == 0
|
||||
assert "no skills found" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_usage_command_is_registered():
|
||||
"""The `usage` subcommand must be wired into the curator argparse tree."""
|
||||
import argparse
|
||||
import hermes_cli.curator as curator_cli
|
||||
|
||||
parser = argparse.ArgumentParser(prog="hermes curator")
|
||||
curator_cli.register_cli(parser)
|
||||
args = parser.parse_args(["usage", "--sort", "recent", "--provenance", "hub", "--json"])
|
||||
assert args.func is curator_cli._cmd_usage
|
||||
assert args.sort == "recent"
|
||||
assert args.provenance == "hub"
|
||||
assert args.json is True
|
||||
Reference in New Issue
Block a user