From ae1f058b3c56b8aa43254382b7e4059cc4b07f63 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 7 May 2026 05:46:51 -0700 Subject: [PATCH] feat(curator): add `hermes curator list-archived` command (#21236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lists the skills sitting in ~/.hermes/skills/.archive/ so users have something to pass to `hermes curator restore`. `curator status` already shows counts; this fills the name-discovery gap. Archive layout is flat (`archive_skill` writes to `.archive//`), so the directory name IS the skill name — no frontmatter parsing needed. Timestamped collision directories (`-`) are listed literally; user can still pass them to `restore`. Reshape of @EvilDrag0n's #20651, simplified: drop the frontmatter rglob + preamble/trailer output + duplicate subcommand registration. Co-authored-by: EvilDrag0n --- hermes_cli/commands.py | 4 ++-- hermes_cli/curator.py | 15 +++++++++++++++ scripts/release.py | 1 + tools/skill_usage.py | 13 +++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 2cf2c3e9f4..6b9f7f92c5 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -157,9 +157,9 @@ COMMAND_REGISTRY: list[CommandDef] = [ CommandDef("cron", "Manage scheduled tasks", "Tools & Skills", cli_only=True, args_hint="[subcommand]", subcommands=("list", "add", "create", "edit", "pause", "resume", "run", "remove")), - CommandDef("curator", "Background skill maintenance (status, run, pin, archive)", + CommandDef("curator", "Background skill maintenance (status, run, pin, archive, list-archived)", "Tools & Skills", args_hint="[subcommand]", - subcommands=("status", "run", "pause", "resume", "pin", "unpin", "restore")), + subcommands=("status", "run", "pause", "resume", "pin", "unpin", "restore", "list-archived")), CommandDef("kanban", "Multi-profile collaboration board (tasks, links, comments)", "Tools & Skills", args_hint="[subcommand]", subcommands=("list", "ls", "show", "create", "assign", "link", "unlink", diff --git a/hermes_cli/curator.py b/hermes_cli/curator.py index ed86a92c26..318c4a0972 100644 --- a/hermes_cli/curator.py +++ b/hermes_cli/curator.py @@ -452,6 +452,18 @@ def _cmd_rollback(args) -> int: return 1 +def _cmd_list_archived(args) -> int: + """List archived (recoverable) skills.""" + from tools import skill_usage + names = skill_usage.list_archived_skill_names() + if not names: + print("curator: no archived skills") + return 0 + for name in names: + print(name) + return 0 + + # --------------------------------------------------------------------------- # argparse wiring (called from hermes_cli.main) # --------------------------------------------------------------------------- @@ -502,6 +514,9 @@ def register_cli(parent: argparse.ArgumentParser) -> None: p_restore.add_argument("skill", help="Skill name") p_restore.set_defaults(func=_cmd_restore) + subs.add_parser("list-archived", help="List archived skills") \ + .set_defaults(func=_cmd_list_archived) + p_archive = subs.add_parser( "archive", help="Manually archive a skill (move to .archive/, excluded from prompt)", diff --git a/scripts/release.py b/scripts/release.py index f46daa92ba..70170b0091 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -870,6 +870,7 @@ AUTHOR_MAP = { "leosma@gmail.com": "leon7609", # PR #19069 "nouseman666@gmail.com": "nouseman666", # PR #19088 "ginwu05@gmail.com": "GinWU05", # PR #19093 + "lxl694522264@gmail.com": "EvilDrag0n", # PR #20651 } diff --git a/tools/skill_usage.py b/tools/skill_usage.py index 053f27b224..9b94ca9a05 100644 --- a/tools/skill_usage.py +++ b/tools/skill_usage.py @@ -205,6 +205,19 @@ def list_agent_created_skill_names() -> List[str]: return sorted(set(names)) +def list_archived_skill_names() -> List[str]: + """Enumerate skills in ``~/.hermes/skills/.archive/``. + + Archive layout is flat (``.archive//``) as set by ``archive_skill``, + so the directory name is the skill name. Used by ``hermes curator + list-archived`` to help users pass a name to ``hermes curator restore``. + """ + archive_root = _archive_dir() + if not archive_root.exists(): + return [] + return sorted({p.name for p in archive_root.iterdir() if p.is_dir()}) + + def _read_skill_name(skill_md: Path, fallback: str) -> str: """Parse the `name:` field from a SKILL.md YAML frontmatter.""" try: