mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
refactor(cli): promote 9 closure handlers to top-level + extract their parsers (god-file Phase 2 follow-up)
Subcommands whose handler was a closure defined inside main() — memory, acp, tools, insights, skills, pairing, plugins, mcp, claw — have their handler promoted to a top-level function and their parser block extracted into hermes_cli/subcommands/<name>.py (build_<name>_parser, injected handler). These 9 had zero closure-over-main-locals, so promotion is a pure relocation. acp/mcp parser blocks use the shared add_accept_hooks_flag helper. main() 1798 -> 954 LOC (71% below the 3297 Phase-2 starting point); add_parser calls in main.py 89 -> 28. Deferred: sessions, computer-use, secrets handlers reference <name>_parser (for a no-subcommand print_help fallback) — left in place to avoid the _self_parser indirection; minority, low value. Behavior-neutral: all 9 subcommands' --help (incl nested subactions) byte- identical to pre-extraction (diff-verified). tests/hermes_cli/ 6519 passed / 0 failed; new test_subcommands_followup.py covers the 9 builders.
This commit is contained in:
parent
524453dab5
commit
1a626470ca
11 changed files with 1067 additions and 862 deletions
1043
hermes_cli/main.py
1043
hermes_cli/main.py
File diff suppressed because it is too large
Load diff
52
hermes_cli/subcommands/acp.py
Normal file
52
hermes_cli/subcommands/acp.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""``hermes acp`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from hermes_cli.subcommands._shared import add_accept_hooks_flag
|
||||
|
||||
|
||||
def build_acp_parser(subparsers, *, cmd_acp: Callable) -> None:
|
||||
"""Attach the ``acp`` subcommand to ``subparsers``."""
|
||||
acp_parser = subparsers.add_parser(
|
||||
"acp",
|
||||
help="Run Hermes Agent as an ACP (Agent Client Protocol) server",
|
||||
description="Start Hermes Agent in ACP mode for editor integration (VS Code, Zed, JetBrains)",
|
||||
)
|
||||
add_accept_hooks_flag(acp_parser)
|
||||
acp_parser.add_argument(
|
||||
"--version",
|
||||
action="store_true",
|
||||
dest="acp_version",
|
||||
help="Print Hermes ACP version and exit",
|
||||
)
|
||||
acp_parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Verify ACP dependencies and adapter imports, then exit",
|
||||
)
|
||||
acp_parser.add_argument(
|
||||
"--setup",
|
||||
action="store_true",
|
||||
help="Run interactive Hermes provider/model setup for ACP terminal auth",
|
||||
)
|
||||
acp_parser.add_argument(
|
||||
"--setup-browser",
|
||||
action="store_true",
|
||||
help="Install agent-browser + Playwright Chromium into ~/.hermes/node/ "
|
||||
"for browser tool support (idempotent).",
|
||||
)
|
||||
acp_parser.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
dest="assume_yes",
|
||||
help="Accept all prompts (used by --setup-browser to skip the "
|
||||
"~400 MB Chromium download confirmation).",
|
||||
)
|
||||
acp_parser.set_defaults(func=cmd_acp)
|
||||
92
hermes_cli/subcommands/claw.py
Normal file
92
hermes_cli/subcommands/claw.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""``hermes claw`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_claw_parser(subparsers, *, cmd_claw: Callable) -> None:
|
||||
"""Attach the ``claw`` subcommand to ``subparsers``."""
|
||||
claw_parser = subparsers.add_parser(
|
||||
"claw",
|
||||
help="OpenClaw migration tools",
|
||||
description="Migrate settings, memories, skills, and API keys from OpenClaw to Hermes",
|
||||
)
|
||||
claw_subparsers = claw_parser.add_subparsers(dest="claw_action")
|
||||
|
||||
# claw migrate
|
||||
claw_migrate = claw_subparsers.add_parser(
|
||||
"migrate",
|
||||
help="Migrate from OpenClaw to Hermes",
|
||||
description="Import settings, memories, skills, and API keys from an OpenClaw installation. "
|
||||
"Always shows a preview before making changes.",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--source", help="Path to OpenClaw directory (default: ~/.openclaw)"
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview only — stop after showing what would be migrated",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--preset",
|
||||
choices=["user-data", "full"],
|
||||
default="full",
|
||||
help="Migration preset (default: full). Neither preset imports secrets — "
|
||||
"pass --migrate-secrets to include API keys.",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite existing files (default: refuse to apply when the plan has conflicts)",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--migrate-secrets",
|
||||
action="store_true",
|
||||
help="Include allowlisted secrets (TELEGRAM_BOT_TOKEN, API keys, etc.). "
|
||||
"Required even under --preset full.",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Skip the pre-migration zip snapshot of ~/.hermes/ (by default a "
|
||||
"single restore-point archive is written to ~/.hermes/backups/ "
|
||||
"before apply; restorable with 'hermes import').",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--workspace-target", help="Absolute path to copy workspace instructions into"
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--skill-conflict",
|
||||
choices=["skip", "overwrite", "rename"],
|
||||
default="skip",
|
||||
help="How to handle skill name conflicts (default: skip)",
|
||||
)
|
||||
claw_migrate.add_argument(
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation prompts"
|
||||
)
|
||||
|
||||
# claw cleanup
|
||||
claw_cleanup = claw_subparsers.add_parser(
|
||||
"cleanup",
|
||||
aliases=["clean"],
|
||||
help="Archive leftover OpenClaw directories after migration",
|
||||
description="Scan for and archive leftover OpenClaw directories to prevent state fragmentation",
|
||||
)
|
||||
claw_cleanup.add_argument(
|
||||
"--source", help="Path to a specific OpenClaw directory to clean up"
|
||||
)
|
||||
claw_cleanup.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview what would be archived without making changes",
|
||||
)
|
||||
claw_cleanup.add_argument(
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation prompts"
|
||||
)
|
||||
claw_parser.set_defaults(func=cmd_claw)
|
||||
25
hermes_cli/subcommands/insights.py
Normal file
25
hermes_cli/subcommands/insights.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""``hermes insights`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_insights_parser(subparsers, *, cmd_insights: Callable) -> None:
|
||||
"""Attach the ``insights`` subcommand to ``subparsers``."""
|
||||
insights_parser = subparsers.add_parser(
|
||||
"insights",
|
||||
help="Show usage insights and analytics",
|
||||
description="Analyze session history to show token usage, costs, tool patterns, and activity trends",
|
||||
)
|
||||
insights_parser.add_argument(
|
||||
"--days", type=int, default=30, help="Number of days to analyze (default: 30)"
|
||||
)
|
||||
insights_parser.add_argument(
|
||||
"--source", help="Filter by platform (cli, telegram, discord, etc.)"
|
||||
)
|
||||
insights_parser.set_defaults(func=cmd_insights)
|
||||
104
hermes_cli/subcommands/mcp.py
Normal file
104
hermes_cli/subcommands/mcp.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""``hermes mcp`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from hermes_cli.subcommands._shared import add_accept_hooks_flag
|
||||
|
||||
|
||||
def build_mcp_parser(subparsers, *, cmd_mcp: Callable) -> None:
|
||||
"""Attach the ``mcp`` subcommand to ``subparsers``."""
|
||||
mcp_parser = subparsers.add_parser(
|
||||
"mcp",
|
||||
help="Manage MCP servers and run Hermes as an MCP server",
|
||||
description=(
|
||||
"Manage MCP server connections and run Hermes as an MCP server.\n\n"
|
||||
"MCP servers provide additional tools via the Model Context Protocol.\n"
|
||||
"Use 'hermes mcp add' to connect to a new server, or\n"
|
||||
"'hermes mcp serve' to expose Hermes conversations over MCP."
|
||||
),
|
||||
)
|
||||
mcp_sub = mcp_parser.add_subparsers(dest="mcp_action")
|
||||
|
||||
mcp_serve_p = mcp_sub.add_parser(
|
||||
"serve",
|
||||
help="Run Hermes as an MCP server (expose conversations to other agents)",
|
||||
)
|
||||
mcp_serve_p.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Enable verbose logging on stderr",
|
||||
)
|
||||
add_accept_hooks_flag(mcp_serve_p)
|
||||
|
||||
mcp_add_p = mcp_sub.add_parser(
|
||||
"add", help="Add an MCP server (discovery-first install)"
|
||||
)
|
||||
mcp_add_p.add_argument("name", help="Server name (used as config key)")
|
||||
mcp_add_p.add_argument("--url", help="HTTP/SSE endpoint URL")
|
||||
# dest="mcp_command" so this flag does not clobber the top-level
|
||||
# subparser's args.command attribute, which the dispatcher reads to
|
||||
# route to cmd_mcp. Without an explicit dest, argparse derives
|
||||
# dest="command" from the flag name and sets it to None when the
|
||||
# flag is omitted, causing `hermes mcp add ...` to fall through to
|
||||
# interactive chat.
|
||||
mcp_add_p.add_argument(
|
||||
"--command", dest="mcp_command", help="Stdio command (e.g. npx)"
|
||||
)
|
||||
mcp_add_p.add_argument(
|
||||
"--args", nargs="*", default=[], help="Arguments for stdio command"
|
||||
)
|
||||
mcp_add_p.add_argument("--auth", choices=["oauth", "header"], help="Auth method")
|
||||
mcp_add_p.add_argument("--preset", help="Known MCP preset name")
|
||||
mcp_add_p.add_argument(
|
||||
"--env",
|
||||
nargs="*",
|
||||
default=[],
|
||||
help="Environment variables for stdio servers (KEY=VALUE)",
|
||||
)
|
||||
|
||||
mcp_rm_p = mcp_sub.add_parser("remove", aliases=["rm"], help="Remove an MCP server")
|
||||
mcp_rm_p.add_argument("name", help="Server name to remove")
|
||||
|
||||
mcp_sub.add_parser("list", aliases=["ls"], help="List configured MCP servers")
|
||||
|
||||
mcp_test_p = mcp_sub.add_parser("test", help="Test MCP server connection")
|
||||
mcp_test_p.add_argument("name", help="Server name to test")
|
||||
|
||||
mcp_cfg_p = mcp_sub.add_parser(
|
||||
"configure", aliases=["config"], help="Toggle tool selection"
|
||||
)
|
||||
mcp_cfg_p.add_argument("name", help="Server name to configure")
|
||||
|
||||
mcp_login_p = mcp_sub.add_parser(
|
||||
"login",
|
||||
help="Force re-authentication for an OAuth-based MCP server",
|
||||
)
|
||||
mcp_login_p.add_argument("name", help="Server name to re-authenticate")
|
||||
|
||||
# ── Catalog (Nous-approved MCPs shipped with the repo) ─────────────────
|
||||
mcp_sub.add_parser(
|
||||
"picker",
|
||||
help="Interactive catalog picker (also the default for `hermes mcp`)",
|
||||
)
|
||||
mcp_sub.add_parser(
|
||||
"catalog",
|
||||
help="List Nous-approved MCPs available for one-click install",
|
||||
)
|
||||
mcp_install_p = mcp_sub.add_parser(
|
||||
"install",
|
||||
help="Install a catalog MCP by name (e.g. `hermes mcp install n8n`)",
|
||||
)
|
||||
mcp_install_p.add_argument(
|
||||
"identifier",
|
||||
help="Catalog entry name (or `official/<name>`)",
|
||||
)
|
||||
|
||||
add_accept_hooks_flag(mcp_parser)
|
||||
mcp_parser.set_defaults(func=cmd_mcp)
|
||||
53
hermes_cli/subcommands/memory.py
Normal file
53
hermes_cli/subcommands/memory.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""``hermes memory`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_memory_parser(subparsers, *, cmd_memory: Callable) -> None:
|
||||
"""Attach the ``memory`` subcommand to ``subparsers``."""
|
||||
memory_parser = subparsers.add_parser(
|
||||
"memory",
|
||||
help="Configure external memory provider",
|
||||
description=(
|
||||
"Set up and manage external memory provider plugins.\n\n"
|
||||
"Available providers: honcho, openviking, mem0, hindsight,\n"
|
||||
"holographic, retaindb, byterover.\n\n"
|
||||
"Only one external provider can be active at a time.\n"
|
||||
"Built-in memory (MEMORY.md/USER.md) is always active."
|
||||
),
|
||||
)
|
||||
memory_sub = memory_parser.add_subparsers(dest="memory_command")
|
||||
_setup_parser = memory_sub.add_parser(
|
||||
"setup", help="Interactive provider selection and configuration"
|
||||
)
|
||||
_setup_parser.add_argument(
|
||||
"provider",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Provider to configure directly (e.g. honcho), skipping the picker",
|
||||
)
|
||||
memory_sub.add_parser("status", help="Show current memory provider config")
|
||||
memory_sub.add_parser("off", help="Disable external provider (built-in only)")
|
||||
_reset_parser = memory_sub.add_parser(
|
||||
"reset",
|
||||
help="Erase all built-in memory (MEMORY.md and USER.md)",
|
||||
)
|
||||
_reset_parser.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt",
|
||||
)
|
||||
_reset_parser.add_argument(
|
||||
"--target",
|
||||
choices=["all", "memory", "user"],
|
||||
default="all",
|
||||
help="Which store to reset: 'all' (default), 'memory', or 'user'",
|
||||
)
|
||||
memory_parser.set_defaults(func=cmd_memory)
|
||||
36
hermes_cli/subcommands/pairing.py
Normal file
36
hermes_cli/subcommands/pairing.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""``hermes pairing`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_pairing_parser(subparsers, *, cmd_pairing: Callable) -> None:
|
||||
"""Attach the ``pairing`` subcommand to ``subparsers``."""
|
||||
pairing_parser = subparsers.add_parser(
|
||||
"pairing",
|
||||
help="Manage DM pairing codes for user authorization",
|
||||
description="Approve or revoke user access via pairing codes",
|
||||
)
|
||||
pairing_sub = pairing_parser.add_subparsers(dest="pairing_action")
|
||||
|
||||
pairing_sub.add_parser("list", help="Show pending + approved users")
|
||||
|
||||
pairing_approve_parser = pairing_sub.add_parser(
|
||||
"approve", help="Approve a pairing code"
|
||||
)
|
||||
pairing_approve_parser.add_argument(
|
||||
"platform", help="Platform name (telegram, discord, slack, whatsapp)"
|
||||
)
|
||||
pairing_approve_parser.add_argument("code", help="Pairing code to approve")
|
||||
|
||||
pairing_revoke_parser = pairing_sub.add_parser("revoke", help="Revoke user access")
|
||||
pairing_revoke_parser.add_argument("platform", help="Platform name")
|
||||
pairing_revoke_parser.add_argument("user_id", help="User ID to revoke")
|
||||
|
||||
pairing_sub.add_parser("clear-pending", help="Clear all pending codes")
|
||||
pairing_parser.set_defaults(func=cmd_pairing)
|
||||
94
hermes_cli/subcommands/plugins.py
Normal file
94
hermes_cli/subcommands/plugins.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""``hermes plugins`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_plugins_parser(subparsers, *, cmd_plugins: Callable) -> None:
|
||||
"""Attach the ``plugins`` subcommand to ``subparsers``."""
|
||||
plugins_parser = subparsers.add_parser(
|
||||
"plugins",
|
||||
help="Manage plugins — install, update, remove, list",
|
||||
description="Install plugins from Git repositories, update, remove, or list them.",
|
||||
)
|
||||
plugins_subparsers = plugins_parser.add_subparsers(dest="plugins_action")
|
||||
|
||||
plugins_install = plugins_subparsers.add_parser(
|
||||
"install", help="Install a plugin from a Git URL or owner/repo"
|
||||
)
|
||||
plugins_install.add_argument(
|
||||
"identifier",
|
||||
help="Git URL or owner/repo shorthand (e.g. anpicasso/hermes-plugin-chrome-profiles)",
|
||||
)
|
||||
plugins_install.add_argument(
|
||||
"--force",
|
||||
"-f",
|
||||
action="store_true",
|
||||
help="Remove existing plugin and reinstall",
|
||||
)
|
||||
_install_enable_group = plugins_install.add_mutually_exclusive_group()
|
||||
_install_enable_group.add_argument(
|
||||
"--enable",
|
||||
action="store_true",
|
||||
help="Auto-enable the plugin after install (skip confirmation prompt)",
|
||||
)
|
||||
_install_enable_group.add_argument(
|
||||
"--no-enable",
|
||||
action="store_true",
|
||||
help="Install disabled (skip confirmation prompt); enable later with `hermes plugins enable <name>`",
|
||||
)
|
||||
|
||||
plugins_update = plugins_subparsers.add_parser(
|
||||
"update", help="Pull latest changes for an installed plugin"
|
||||
)
|
||||
plugins_update.add_argument("name", help="Plugin name to update")
|
||||
|
||||
plugins_remove = plugins_subparsers.add_parser(
|
||||
"remove", aliases=["rm", "uninstall"], help="Remove an installed plugin"
|
||||
)
|
||||
plugins_remove.add_argument("name", help="Plugin directory name to remove")
|
||||
|
||||
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"
|
||||
)
|
||||
plugins_enable.add_argument("name", help="Plugin name to enable")
|
||||
|
||||
plugins_disable = plugins_subparsers.add_parser(
|
||||
"disable", help="Disable a plugin without removing it"
|
||||
)
|
||||
plugins_disable.add_argument("name", help="Plugin name to disable")
|
||||
plugins_parser.set_defaults(func=cmd_plugins)
|
||||
269
hermes_cli/subcommands/skills.py
Normal file
269
hermes_cli/subcommands/skills.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
"""``hermes skills`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
"""Attach the ``skills`` subcommand to ``subparsers``."""
|
||||
skills_parser = subparsers.add_parser(
|
||||
"skills",
|
||||
help="Search, install, configure, and manage skills",
|
||||
description="Search, install, inspect, audit, configure, and manage skills from skills.sh, well-known agent skill endpoints, GitHub, ClawHub, and other registries.",
|
||||
)
|
||||
skills_subparsers = skills_parser.add_subparsers(dest="skills_action")
|
||||
|
||||
skills_browse = skills_subparsers.add_parser(
|
||||
"browse", help="Browse all available skills (paginated)"
|
||||
)
|
||||
skills_browse.add_argument(
|
||||
"--page", type=int, default=1, help="Page number (default: 1)"
|
||||
)
|
||||
skills_browse.add_argument(
|
||||
"--size", type=int, default=20, help="Results per page (default: 20)"
|
||||
)
|
||||
skills_browse.add_argument(
|
||||
"--source",
|
||||
default="all",
|
||||
choices=[
|
||||
"all",
|
||||
"official",
|
||||
"skills-sh",
|
||||
"well-known",
|
||||
"github",
|
||||
"clawhub",
|
||||
"lobehub",
|
||||
"browse-sh",
|
||||
],
|
||||
help="Filter by source (default: all)",
|
||||
)
|
||||
|
||||
skills_search = skills_subparsers.add_parser(
|
||||
"search", help="Search skill registries"
|
||||
)
|
||||
skills_search.add_argument("query", help="Search query")
|
||||
skills_search.add_argument(
|
||||
"--source",
|
||||
default="all",
|
||||
choices=[
|
||||
"all",
|
||||
"official",
|
||||
"skills-sh",
|
||||
"well-known",
|
||||
"github",
|
||||
"clawhub",
|
||||
"lobehub",
|
||||
"browse-sh",
|
||||
],
|
||||
)
|
||||
skills_search.add_argument("--limit", type=int, default=10, help="Max results")
|
||||
skills_search.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output JSON instead of a table (full identifiers, scripting-friendly)",
|
||||
)
|
||||
|
||||
skills_install = skills_subparsers.add_parser("install", help="Install a skill")
|
||||
skills_install.add_argument(
|
||||
"identifier",
|
||||
help="Skill identifier (e.g. openai/skills/skill-creator) or a direct HTTP(S) URL to a SKILL.md file",
|
||||
)
|
||||
skills_install.add_argument(
|
||||
"--category", default="", help="Category folder to install into"
|
||||
)
|
||||
skills_install.add_argument(
|
||||
"--name",
|
||||
default="",
|
||||
help="Override the skill name (useful when installing from a URL whose SKILL.md has no `name:` frontmatter)",
|
||||
)
|
||||
skills_install.add_argument(
|
||||
"--force", action="store_true", help="Install despite blocked scan verdict"
|
||||
)
|
||||
skills_install.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt (needed in TUI mode)",
|
||||
)
|
||||
|
||||
skills_inspect = skills_subparsers.add_parser(
|
||||
"inspect", help="Preview a skill without installing"
|
||||
)
|
||||
skills_inspect.add_argument("identifier", help="Skill identifier")
|
||||
|
||||
skills_list = skills_subparsers.add_parser("list", help="List installed skills")
|
||||
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"
|
||||
)
|
||||
skills_check.add_argument(
|
||||
"name", nargs="?", help="Specific skill to check (default: all)"
|
||||
)
|
||||
|
||||
skills_update = skills_subparsers.add_parser(
|
||||
"update", help="Update installed hub skills"
|
||||
)
|
||||
skills_update.add_argument(
|
||||
"name",
|
||||
nargs="?",
|
||||
help="Specific skill to update (default: all outdated skills)",
|
||||
)
|
||||
|
||||
skills_audit = skills_subparsers.add_parser(
|
||||
"audit", help="Re-scan installed hub skills"
|
||||
)
|
||||
skills_audit.add_argument(
|
||||
"name", nargs="?", help="Specific skill to audit (default: all)"
|
||||
)
|
||||
skills_audit.add_argument(
|
||||
"--deep",
|
||||
action="store_true",
|
||||
help="Run AST-level analysis on Python files (opt-in diagnostic)",
|
||||
)
|
||||
|
||||
skills_uninstall = skills_subparsers.add_parser(
|
||||
"uninstall", help="Remove a hub-installed skill"
|
||||
)
|
||||
skills_uninstall.add_argument("name", help="Skill name to remove")
|
||||
|
||||
skills_reset = skills_subparsers.add_parser(
|
||||
"reset",
|
||||
help="Reset a bundled skill — clears 'user-modified' tracking so updates work again",
|
||||
description=(
|
||||
"Clear a bundled skill's entry from the sync manifest (~/.hermes/skills/.bundled_manifest) "
|
||||
"so future 'hermes update' runs stop marking it as user-modified. Pass --restore to also "
|
||||
"replace the current copy with the bundled version."
|
||||
),
|
||||
)
|
||||
skills_reset.add_argument(
|
||||
"name", help="Skill name to reset (e.g. google-workspace)"
|
||||
)
|
||||
skills_reset.add_argument(
|
||||
"--restore",
|
||||
action="store_true",
|
||||
help="Also delete the current copy and re-copy the bundled version",
|
||||
)
|
||||
skills_reset.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt when using --restore",
|
||||
)
|
||||
|
||||
skills_opt_out = skills_subparsers.add_parser(
|
||||
"opt-out",
|
||||
help="Stop bundled skills from being seeded into this profile",
|
||||
description=(
|
||||
"Write the .no-bundled-skills marker so the installer, "
|
||||
"`hermes update`, and any direct sync stop seeding bundled skills "
|
||||
"into the active profile. By default nothing already on disk is "
|
||||
"touched. Pass --remove to ALSO delete bundled skills that are "
|
||||
"unmodified (user-edited and hub/local skills are never removed)."
|
||||
),
|
||||
)
|
||||
skills_opt_out.add_argument(
|
||||
"--remove",
|
||||
action="store_true",
|
||||
help="Also delete already-present unmodified bundled skills",
|
||||
)
|
||||
skills_opt_out.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt when using --remove",
|
||||
)
|
||||
|
||||
skills_opt_in = skills_subparsers.add_parser(
|
||||
"opt-in",
|
||||
help="Re-enable bundled-skill seeding (undo opt-out)",
|
||||
description=(
|
||||
"Remove the .no-bundled-skills marker so bundled skills are seeded "
|
||||
"again on the next `hermes update`. Pass --sync to re-seed now."
|
||||
),
|
||||
)
|
||||
skills_opt_in.add_argument(
|
||||
"--sync",
|
||||
action="store_true",
|
||||
help="Re-seed bundled skills immediately instead of waiting for update",
|
||||
)
|
||||
|
||||
skills_repair_official = skills_subparsers.add_parser(
|
||||
"repair-official",
|
||||
help="Backfill or restore official optional skills from repo source",
|
||||
description=(
|
||||
"Repair official optional skill provenance. By default, only backfills "
|
||||
"hub metadata for exact matches. Pass --restore to replace missing or "
|
||||
"mutated active copies from optional-skills/, moving existing copies to "
|
||||
"a restore backup first. Use name 'all' to repair every optional skill."
|
||||
),
|
||||
)
|
||||
skills_repair_official.add_argument(
|
||||
"name", help="Official optional skill folder/frontmatter name, or 'all'"
|
||||
)
|
||||
skills_repair_official.add_argument(
|
||||
"--restore",
|
||||
action="store_true",
|
||||
help="Restore from official optional source, backing up existing matching copies",
|
||||
)
|
||||
skills_repair_official.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt when using --restore",
|
||||
)
|
||||
|
||||
skills_publish = skills_subparsers.add_parser(
|
||||
"publish", help="Publish a skill to a registry"
|
||||
)
|
||||
skills_publish.add_argument("skill_path", help="Path to skill directory")
|
||||
skills_publish.add_argument(
|
||||
"--to", default="github", choices=["github", "clawhub"], help="Target registry"
|
||||
)
|
||||
skills_publish.add_argument(
|
||||
"--repo", default="", help="Target GitHub repo (e.g. openai/skills)"
|
||||
)
|
||||
|
||||
skills_snapshot = skills_subparsers.add_parser(
|
||||
"snapshot", help="Export/import skill configurations"
|
||||
)
|
||||
snapshot_subparsers = skills_snapshot.add_subparsers(dest="snapshot_action")
|
||||
snap_export = snapshot_subparsers.add_parser(
|
||||
"export", help="Export installed skills to a file"
|
||||
)
|
||||
snap_export.add_argument("output", help="Output JSON file path (use - for stdout)")
|
||||
snap_import = snapshot_subparsers.add_parser(
|
||||
"import", help="Import and install skills from a file"
|
||||
)
|
||||
snap_import.add_argument("input", help="Input JSON file path")
|
||||
snap_import.add_argument(
|
||||
"--force", action="store_true", help="Force install despite caution verdict"
|
||||
)
|
||||
|
||||
skills_tap = skills_subparsers.add_parser("tap", help="Manage skill sources")
|
||||
tap_subparsers = skills_tap.add_subparsers(dest="tap_action")
|
||||
tap_subparsers.add_parser("list", help="List configured taps")
|
||||
tap_add = tap_subparsers.add_parser("add", help="Add a GitHub repo as skill source")
|
||||
tap_add.add_argument("repo", help="GitHub repo (e.g. owner/repo)")
|
||||
tap_rm = tap_subparsers.add_parser("remove", help="Remove a tap")
|
||||
tap_rm.add_argument("name", help="Tap name to remove")
|
||||
|
||||
# config sub-action: interactive enable/disable
|
||||
skills_subparsers.add_parser(
|
||||
"config",
|
||||
help="Interactive skill configuration — enable/disable individual skills",
|
||||
)
|
||||
skills_parser.set_defaults(func=cmd_skills)
|
||||
95
hermes_cli/subcommands/tools.py
Normal file
95
hermes_cli/subcommands/tools.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""``hermes tools`` subcommand parser.
|
||||
|
||||
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_tools_parser(subparsers, *, cmd_tools: Callable) -> None:
|
||||
"""Attach the ``tools`` subcommand to ``subparsers``."""
|
||||
tools_parser = subparsers.add_parser(
|
||||
"tools",
|
||||
help="Configure which tools are enabled per platform",
|
||||
description=(
|
||||
"Enable, disable, or list tools for CLI, Telegram, Discord, etc.\n\n"
|
||||
"Built-in toolsets use plain names (e.g. web, memory).\n"
|
||||
"MCP tools use server:tool notation (e.g. github:create_issue).\n\n"
|
||||
"Run 'hermes tools' with no subcommand for the interactive configuration UI."
|
||||
),
|
||||
)
|
||||
tools_parser.add_argument(
|
||||
"--summary",
|
||||
action="store_true",
|
||||
help="Print a summary of enabled tools per platform and exit",
|
||||
)
|
||||
tools_sub = tools_parser.add_subparsers(dest="tools_action")
|
||||
|
||||
# hermes tools list [--platform cli]
|
||||
tools_list_p = tools_sub.add_parser(
|
||||
"list",
|
||||
help="Show all tools and their enabled/disabled status",
|
||||
)
|
||||
tools_list_p.add_argument(
|
||||
"--platform",
|
||||
default="cli",
|
||||
help="Platform to show (default: cli)",
|
||||
)
|
||||
|
||||
# hermes tools disable <name...> [--platform cli]
|
||||
tools_disable_p = tools_sub.add_parser(
|
||||
"disable",
|
||||
help="Disable toolsets or MCP tools",
|
||||
)
|
||||
tools_disable_p.add_argument(
|
||||
"names",
|
||||
nargs="+",
|
||||
metavar="NAME",
|
||||
help="Toolset name (e.g. web) or MCP tool in server:tool form",
|
||||
)
|
||||
tools_disable_p.add_argument(
|
||||
"--platform",
|
||||
default="cli",
|
||||
help="Platform to apply to (default: cli)",
|
||||
)
|
||||
|
||||
# hermes tools enable <name...> [--platform cli]
|
||||
tools_enable_p = tools_sub.add_parser(
|
||||
"enable",
|
||||
help="Enable toolsets or MCP tools",
|
||||
)
|
||||
tools_enable_p.add_argument(
|
||||
"names",
|
||||
nargs="+",
|
||||
metavar="NAME",
|
||||
help="Toolset name or MCP tool in server:tool form",
|
||||
)
|
||||
tools_enable_p.add_argument(
|
||||
"--platform",
|
||||
default="cli",
|
||||
help="Platform to apply to (default: cli)",
|
||||
)
|
||||
|
||||
# hermes tools post-setup <key>
|
||||
tools_postsetup_p = tools_sub.add_parser(
|
||||
"post-setup",
|
||||
help="Run a provider's post-setup install hook (npm/pip/binary)",
|
||||
description=(
|
||||
"Run the install/bootstrap hook a tool backend declares — the\n"
|
||||
"same step `hermes tools` runs after you pick a provider that\n"
|
||||
"needs extra dependencies (browser Chromium, Camofox, cua-driver,\n"
|
||||
"KittenTTS/Piper, ddgs, Spotify, Langfuse, xAI). Stable,\n"
|
||||
"non-interactive target the dashboard spawns to drive backend\n"
|
||||
"setup. Keys: agent_browser, camofox, cua_driver, kittentts,\n"
|
||||
"piper, ddgs, spotify, langfuse, xai_grok."
|
||||
),
|
||||
)
|
||||
tools_postsetup_p.add_argument(
|
||||
"post_setup_key",
|
||||
metavar="KEY",
|
||||
help="Post-setup hook key (e.g. agent_browser, camofox, kittentts)",
|
||||
)
|
||||
tools_parser.set_defaults(func=cmd_tools)
|
||||
66
tests/hermes_cli/test_subcommands_followup.py
Normal file
66
tests/hermes_cli/test_subcommands_followup.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Smoke tests for the Phase 2 follow-up subcommand builders (promoted handlers).
|
||||
|
||||
These 9 subcommands had their handler defined as a closure inside main(); the
|
||||
handler was promoted to top-level and the parser block extracted into a builder.
|
||||
Confirms each builder attaches its subcommand and wires func to the injected
|
||||
handler.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.subcommands.acp import build_acp_parser
|
||||
from hermes_cli.subcommands.claw import build_claw_parser
|
||||
from hermes_cli.subcommands.insights import build_insights_parser
|
||||
from hermes_cli.subcommands.mcp import build_mcp_parser
|
||||
from hermes_cli.subcommands.memory import build_memory_parser
|
||||
from hermes_cli.subcommands.pairing import build_pairing_parser
|
||||
from hermes_cli.subcommands.plugins import build_plugins_parser
|
||||
from hermes_cli.subcommands.skills import build_skills_parser
|
||||
from hermes_cli.subcommands.tools import build_tools_parser
|
||||
|
||||
|
||||
def _h(name):
|
||||
def handler(args): # pragma: no cover - identity only
|
||||
return name
|
||||
handler.__name__ = f"cmd_{name}"
|
||||
return handler
|
||||
|
||||
|
||||
# (subcommand, builder, handler_kwarg, sample argv that should dispatch to func)
|
||||
CASES = [
|
||||
("memory", build_memory_parser, "cmd_memory", ["memory"]),
|
||||
("acp", build_acp_parser, "cmd_acp", ["acp"]),
|
||||
("tools", build_tools_parser, "cmd_tools", ["tools"]),
|
||||
("insights", build_insights_parser, "cmd_insights", ["insights"]),
|
||||
("skills", build_skills_parser, "cmd_skills", ["skills"]),
|
||||
("pairing", build_pairing_parser, "cmd_pairing", ["pairing"]),
|
||||
("plugins", build_plugins_parser, "cmd_plugins", ["plugins"]),
|
||||
("mcp", build_mcp_parser, "cmd_mcp", ["mcp"]),
|
||||
("claw", build_claw_parser, "cmd_claw", ["claw"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,builder,kw,argv", CASES, ids=[c[0] for c in CASES])
|
||||
def test_followup_builders_dispatch(name, builder, kw, argv):
|
||||
parser = argparse.ArgumentParser(prog="hermes")
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
handler = _h(name)
|
||||
builder(sub, **{kw: handler})
|
||||
ns = parser.parse_args(argv)
|
||||
assert ns.command == name
|
||||
assert ns.func is handler
|
||||
|
||||
|
||||
def test_mcp_and_acp_accept_hooks_flag():
|
||||
# mcp/acp parser blocks use the shared add_accept_hooks_flag helper.
|
||||
parser = argparse.ArgumentParser(prog="hermes")
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
build_mcp_parser(sub, cmd_mcp=_h("mcp"))
|
||||
build_acp_parser(sub, cmd_acp=_h("acp"))
|
||||
# acp takes --accept-hooks at top level
|
||||
ns = parser.parse_args(["acp", "--accept-hooks"])
|
||||
assert ns.accept_hooks is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue