Hermes-agent

This commit is contained in:
Zakaria
2026-06-14 14:30:48 -04:00
commit dac4b88b94
5058 changed files with 1884848 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
"""CLI subcommand parser builders for ``hermes <subcommand>``.
``hermes_cli/main.py:main()`` historically built the entire argparse tree
inline — 179 ``add_parser`` calls across ~26 subcommand groups, all wedged
into one 3,300-line function. This package breaks that tree apart: each
subcommand group owns a ``build_<group>_parser(subparsers, ...)`` function in
its own module, and ``main()`` calls those builders instead of inlining the
argument definitions.
Handlers (the ``cmd_*`` functions) still live in ``main.py`` for now and are
dependency-injected into the builders so these modules never import ``main``
(which would create a cycle). Shared parser helpers live in
``_shared.py``.
Part of the god-file decomposition plan (Phase 2).
"""
from __future__ import annotations
+29
View File
@@ -0,0 +1,29 @@
"""Shared parser helpers used across multiple CLI subcommand builders.
These were module-level helpers in ``hermes_cli/main.py``. They are pulled
into a neutral module so both ``main.py`` and every
``hermes_cli/subcommands/<group>.py`` builder can import them without an
import cycle. ``main.py`` re-exports them for backwards compatibility, so
existing references keep working.
"""
from __future__ import annotations
import argparse
def add_accept_hooks_flag(parser: argparse.ArgumentParser) -> None:
"""Attach the ``--accept-hooks`` flag.
Shared across every agent subparser so the flag works regardless of CLI
position.
"""
parser.add_argument(
"--accept-hooks",
action="store_true",
default=argparse.SUPPRESS,
help=(
"Auto-approve unseen shell hooks without a TTY prompt "
"(equivalent to HERMES_ACCEPT_HOOKS=1 / hooks_auto_accept: true)."
),
)
+52
View 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)
+109
View File
@@ -0,0 +1,109 @@
"""``hermes auth`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_auth_parser(subparsers, *, cmd_auth: Callable) -> None:
"""Attach the ``auth`` subcommand to ``subparsers``."""
auth_parser = subparsers.add_parser(
"auth",
help="Manage pooled provider credentials",
)
auth_subparsers = auth_parser.add_subparsers(dest="auth_action")
auth_add = auth_subparsers.add_parser("add", help="Add a pooled credential")
auth_add.add_argument(
"provider",
help="Provider id (for example: anthropic, openai-codex, openrouter)",
)
auth_add.add_argument(
"--type",
dest="auth_type",
choices=["oauth", "api-key", "api_key"],
help="Credential type to add",
)
auth_add.add_argument("--label", help="Optional display label")
auth_add.add_argument(
"--api-key", help="API key value (otherwise prompted securely)"
)
auth_add.add_argument("--portal-url", help="Nous portal base URL")
auth_add.add_argument("--inference-url", help="Nous inference base URL")
auth_add.add_argument("--client-id", help="OAuth client id")
auth_add.add_argument("--scope", help="OAuth scope override")
auth_add.add_argument(
"--no-browser",
action="store_true",
help="Do not auto-open a browser for OAuth login",
)
auth_add.add_argument(
"--manual-paste",
action="store_true",
help=(
"Skip the loopback callback listener and paste the failed "
"callback URL from your browser instead. Use this on "
"browser-only remotes (GCP Cloud Shell, GitHub Codespaces, "
"EC2 Instance Connect, ...) where 127.0.0.1 on the remote "
"isn't reachable from your laptop. See #26923."
),
)
auth_add.add_argument(
"--timeout", type=float, help="OAuth/network timeout in seconds"
)
auth_add.add_argument(
"--insecure",
action="store_true",
help="Disable TLS verification for OAuth login",
)
auth_add.add_argument("--ca-bundle", help="Custom CA bundle for OAuth login")
auth_list = auth_subparsers.add_parser("list", help="List pooled credentials")
auth_list.add_argument("provider", nargs="?", help="Optional provider filter")
auth_remove = auth_subparsers.add_parser(
"remove", help="Remove a pooled credential by index, id, or label"
)
auth_remove.add_argument("provider", help="Provider id")
auth_remove.add_argument(
"target", help="Credential index, entry id, or exact label"
)
auth_reset = auth_subparsers.add_parser(
"reset", help="Clear exhaustion status for all credentials for a provider"
)
auth_reset.add_argument("provider", help="Provider id")
auth_status = auth_subparsers.add_parser(
"status", help="Show auth status for a provider"
)
auth_status.add_argument("provider", help="Provider id")
auth_logout = auth_subparsers.add_parser(
"logout", help="Log out a provider and clear stored auth state"
)
auth_logout.add_argument("provider", help="Provider id")
auth_spotify = auth_subparsers.add_parser(
"spotify", help="Authenticate Hermes with Spotify via PKCE"
)
auth_spotify.add_argument(
"spotify_action",
nargs="?",
choices=["login", "status", "logout"],
default="login",
)
auth_spotify.add_argument(
"--client-id", help="Spotify app client_id (or set HERMES_SPOTIFY_CLIENT_ID)"
)
auth_spotify.add_argument(
"--redirect-uri",
help="Allow-listed localhost redirect URI for your Spotify app",
)
auth_spotify.add_argument("--scope", help="Override requested Spotify scopes")
auth_spotify.add_argument(
"--no-browser",
action="store_true",
help="Do not attempt to open the browser automatically",
)
auth_spotify.add_argument(
"--timeout", type=float, help="Callback/token exchange timeout in seconds"
)
auth_parser.set_defaults(func=cmd_auth)
+38
View File
@@ -0,0 +1,38 @@
"""``hermes backup`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_backup_parser(subparsers, *, cmd_backup: Callable) -> None:
"""Attach the ``backup`` subcommand to ``subparsers``."""
# =========================================================================
# backup command
# =========================================================================
backup_parser = subparsers.add_parser(
"backup",
help="Back up Hermes home directory to a zip file",
description="Create a zip archive of your entire Hermes configuration, "
"skills, sessions, and data (excludes the hermes-agent codebase). "
"Use --quick for a fast snapshot of just critical state files.",
)
backup_parser.add_argument(
"-o",
"--output",
help="Output path for the zip file (default: ~/hermes-backup-<timestamp>.zip)",
)
backup_parser.add_argument(
"-q",
"--quick",
action="store_true",
help="Quick snapshot: only critical state files (config, state.db, .env, auth, cron)",
)
backup_parser.add_argument(
"-l", "--label", help="Label for the snapshot (only used with --quick)"
)
backup_parser.set_defaults(func=cmd_backup)
+92
View 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)
+49
View File
@@ -0,0 +1,49 @@
"""``hermes config`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_config_parser(subparsers, *, cmd_config: Callable) -> None:
"""Attach the ``config`` subcommand to ``subparsers``."""
# =========================================================================
# config command
# =========================================================================
config_parser = subparsers.add_parser(
"config",
help="View and edit configuration",
description="Manage Hermes Agent configuration",
)
config_subparsers = config_parser.add_subparsers(dest="config_command")
# config show (default)
config_subparsers.add_parser("show", help="Show current configuration")
# config edit
config_subparsers.add_parser("edit", help="Open config file in editor")
# config set
config_set = config_subparsers.add_parser("set", help="Set a configuration value")
config_set.add_argument(
"key", nargs="?", help="Configuration key (e.g., model, terminal.backend)"
)
config_set.add_argument("value", nargs="?", help="Value to set")
# config path
config_subparsers.add_parser("path", help="Print config file path")
# config env-path
config_subparsers.add_parser("env-path", help="Print .env file path")
# config check
config_subparsers.add_parser("check", help="Check for missing/outdated config")
# config migrate
config_subparsers.add_parser("migrate", help="Update config with new options")
config_parser.set_defaults(func=cmd_config)
+163
View File
@@ -0,0 +1,163 @@
"""``hermes cron`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` — same arguments, same
``func=cmd_cron`` dispatch. The handler is injected so this module does not
import ``main`` (cycle avoidance).
"""
from __future__ import annotations
from typing import Callable
from hermes_cli.subcommands._shared import add_accept_hooks_flag
def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
"""Attach the ``cron`` subcommand (and its sub-actions) to ``subparsers``."""
cron_parser = subparsers.add_parser(
"cron", help="Cron job management", description="Manage scheduled tasks"
)
cron_subparsers = cron_parser.add_subparsers(dest="cron_command")
# cron list
cron_list = cron_subparsers.add_parser("list", help="List scheduled jobs")
cron_list.add_argument("--all", action="store_true", help="Include disabled jobs")
# cron create/add
cron_create = cron_subparsers.add_parser(
"create", aliases=["add"], help="Create a scheduled job"
)
cron_create.add_argument(
"schedule", help="Schedule like '30m', 'every 2h', or '0 9 * * *'"
)
cron_create.add_argument(
"prompt", nargs="?", help="Optional self-contained prompt or task instruction"
)
cron_create.add_argument("--name", help="Optional human-friendly job name")
cron_create.add_argument(
"--deliver",
help="Delivery target: origin, local, telegram, discord, signal, or platform:chat_id",
)
cron_create.add_argument("--repeat", type=int, help="Optional repeat count")
cron_create.add_argument(
"--skill",
dest="skills",
action="append",
help="Attach a skill. Repeat to add multiple skills.",
)
cron_create.add_argument(
"--script",
help=(
"Path to a script under ~/.hermes/scripts/. Default mode: "
"script stdout is injected into the agent's prompt each run. "
"With --no-agent: the script IS the job and its stdout is "
"delivered verbatim. .sh/.bash files run via bash, everything "
"else via Python."
),
)
cron_create.add_argument(
"--no-agent",
dest="no_agent",
action="store_true",
default=False,
help=(
"Skip the LLM entirely — run --script on schedule and deliver "
"its stdout directly. Empty stdout = silent. Classic watchdog "
"pattern (memory alerts, disk alerts, CI pings)."
),
)
cron_create.add_argument(
"--workdir",
help="Absolute path for the job to run from. Injects AGENTS.md / CLAUDE.md / .cursorrules from that directory and uses it as the cwd for terminal/file/code_exec tools. Omit to preserve old behaviour (no project context files).",
)
# cron edit
cron_edit = cron_subparsers.add_parser(
"edit", help="Edit an existing scheduled job"
)
cron_edit.add_argument("job_id", help="Job ID to edit")
cron_edit.add_argument("--schedule", help="New schedule")
cron_edit.add_argument("--prompt", help="New prompt/task instruction")
cron_edit.add_argument("--name", help="New job name")
cron_edit.add_argument("--deliver", help="New delivery target")
cron_edit.add_argument("--repeat", type=int, help="New repeat count")
cron_edit.add_argument(
"--skill",
dest="skills",
action="append",
help="Replace the job's skills with this set. Repeat to attach multiple skills.",
)
cron_edit.add_argument(
"--add-skill",
dest="add_skills",
action="append",
help="Append a skill without replacing the existing list. Repeatable.",
)
cron_edit.add_argument(
"--remove-skill",
dest="remove_skills",
action="append",
help="Remove a specific attached skill. Repeatable.",
)
cron_edit.add_argument(
"--clear-skills",
action="store_true",
help="Remove all attached skills from the job",
)
cron_edit.add_argument(
"--script",
help=(
"Path to a script under ~/.hermes/scripts/. Pass empty string to clear. "
"With --no-agent the script IS the job; otherwise its stdout is "
"injected into the agent's prompt each run."
),
)
cron_edit.add_argument(
"--no-agent",
dest="no_agent",
action="store_const",
const=True,
default=None,
help=(
"Enable no-agent mode on this job (requires --script or an "
"existing script on the job)."
),
)
cron_edit.add_argument(
"--agent",
dest="no_agent",
action="store_const",
const=False,
help="Disable no-agent mode on this job (reverts to LLM-driven execution).",
)
cron_edit.add_argument(
"--workdir",
help="Absolute path for the job to run from (injects AGENTS.md etc. and sets terminal cwd). Pass empty string to clear.",
)
# lifecycle actions
cron_pause = cron_subparsers.add_parser("pause", help="Pause a scheduled job")
cron_pause.add_argument("job_id", help="Job ID to pause")
cron_resume = cron_subparsers.add_parser("resume", help="Resume a paused job")
cron_resume.add_argument("job_id", help="Job ID to resume")
cron_run = cron_subparsers.add_parser(
"run", help="Run a job on the next scheduler tick"
)
cron_run.add_argument("job_id", help="Job ID to trigger")
add_accept_hooks_flag(cron_run)
cron_remove = cron_subparsers.add_parser(
"remove", aliases=["rm", "delete"], help="Remove a scheduled job"
)
cron_remove.add_argument("job_id", help="Job ID to remove")
# cron status
cron_subparsers.add_parser("status", help="Check if cron scheduler is running")
# cron tick (mostly for debugging)
cron_tick = cron_subparsers.add_parser("tick", help="Run due jobs once and exit")
add_accept_hooks_flag(cron_tick)
add_accept_hooks_flag(cron_parser)
cron_parser.set_defaults(func=cmd_cron)
+143
View File
@@ -0,0 +1,143 @@
"""``hermes dashboard`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
import argparse
from typing import Callable
def build_dashboard_parser(
subparsers, *, cmd_dashboard: Callable, cmd_dashboard_register: Callable
) -> None:
"""Attach the ``dashboard`` subcommand (and its ``register`` action)."""
# =========================================================================
# dashboard command
# =========================================================================
dashboard_parser = subparsers.add_parser(
"dashboard",
help="Start the web UI dashboard",
description="Launch the Hermes Agent web dashboard for managing config, API keys, and sessions",
)
dashboard_parser.add_argument(
"--port", type=int, default=9119, help="Port (default 9119, 0 for auto-assign by OS)"
)
dashboard_parser.add_argument(
"--host", default="127.0.0.1", help="Host (default 127.0.0.1)"
)
dashboard_parser.add_argument(
"--no-open", action="store_true", help="Don't open browser automatically"
)
dashboard_parser.add_argument(
"--insecure",
action="store_true",
help="Allow binding to non-localhost (DANGEROUS: exposes API keys on the network)",
)
dashboard_parser.add_argument(
"--skip-build",
action="store_true",
help=(
"Skip the web UI build step and serve the existing dist directly. "
"Useful for non-interactive contexts (Windows Scheduled Tasks, CI) "
"where npm may not be available. Pre-build with: cd web && npm run build"
),
)
dashboard_parser.add_argument(
"--isolated",
action="store_true",
help=(
"When launched from a named profile (e.g. `worker dashboard`), run "
"a dedicated dashboard server scoped to that profile instead of "
"routing to the machine dashboard. Default behavior is unified: "
"profile launches attach to (or start) ONE machine-level dashboard "
"and preselect the profile in the UI's profile switcher."
),
)
# Internal flag set by the unified-launch re-exec (cmd_dashboard) to
# preselect the launching profile in the SPA switcher. Hidden from
# --help: users get this behavior automatically via `<profile> dashboard`.
dashboard_parser.add_argument(
"--open-profile",
dest="open_profile",
default="",
help=argparse.SUPPRESS,
)
# Lifecycle flags — mutually exclusive with each other and with the
# start-a-server flags above (if both are passed, --stop / --status win
# because they exit before the server is started). The dashboard has
# no service manager and no PID file, so these scan the process table
# for `hermes dashboard` cmdlines and SIGTERM them directly — the same
# path `hermes update` uses to clean up stale dashboards.
dashboard_parser.add_argument(
"--stop",
action="store_true",
help="Stop all running hermes dashboard processes and exit",
)
dashboard_parser.add_argument(
"--status",
action="store_true",
help="List running hermes dashboard processes and exit",
)
# Backward-compat shim: older Hermes desktop app shells (<= 0.15.x) spawn the
# backend as `hermes dashboard --no-open --tui --host ... --port ...`. The
# `--tui` flag was removed from this subcommand in cae6b5486 (embedded chat is
# always on now). When a user's CLI updates past that commit but their desktop
# app binary has not, argparse used to hard-error with "unrecognized arguments:
# --tui" and exit(2) — the backend died before becoming ready and the GUI just
# showed "Hermes couldn't start" with no actionable cause. Accept and silently
# ignore the flag so an old app + new CLI degrades gracefully instead of
# bricking. Hidden from --help; safe to delete once the floor app version is
# well past 0.16.0.
dashboard_parser.add_argument(
"--tui",
action="store_true",
help=argparse.SUPPRESS,
)
dashboard_parser.set_defaults(func=cmd_dashboard)
# `hermes dashboard register` — register a self-hosted dashboard OAuth
# client with Nous Portal and write the client_id into ~/.hermes/.env.
# Nested subparser so bare `hermes dashboard` keeps launching the server
# (set_defaults(func=cmd_dashboard) above remains the default).
dashboard_subparsers = dashboard_parser.add_subparsers(
dest="dashboard_subcommand"
)
dashboard_register_parser = dashboard_subparsers.add_parser(
"register",
help="Register a self-hosted dashboard with Nous Portal (writes the OAuth client ID to .env)",
description=(
"Register this install as a self-hosted dashboard with your Nous "
"Portal account. Creates an OAuth client, writes "
"HERMES_DASHBOARD_OAUTH_CLIENT_ID into ~/.hermes/.env, and prints "
"how to engage the login gate. Requires being logged in (hermes setup)."
),
)
dashboard_register_parser.add_argument(
"--name",
default=None,
help="Human-readable label for the dashboard (default: an auto-generated name)",
)
dashboard_register_parser.add_argument(
"--redirect-uri",
dest="redirect_uri",
default=None,
help=(
"Optional public HTTPS OAuth redirect URI for the dashboard, e.g. "
"https://hermes.example.com/auth/callback. Omit for localhost-only use."
),
)
dashboard_register_parser.add_argument(
"--portal-url",
dest="portal_url",
default=None,
help=(
"Override the Nous Portal base URL for registration (default: the "
"portal you logged into). The access token must be valid at this "
"portal. Also settable via HERMES_DASHBOARD_PORTAL_URL. Mainly for "
"testing against a staging/preview portal."
),
)
dashboard_register_parser.set_defaults(func=cmd_dashboard_register)
+77
View File
@@ -0,0 +1,77 @@
"""``hermes debug`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
import argparse
from typing import Callable
def build_debug_parser(subparsers, *, cmd_debug: Callable) -> None:
"""Attach the ``debug`` subcommand to ``subparsers``."""
# =========================================================================
# debug command
# =========================================================================
debug_parser = subparsers.add_parser(
"debug",
help="Debug tools — upload logs and system info for support",
description="Debug utilities for Hermes Agent. Use 'hermes debug share' to "
"upload a debug report (system info + recent logs) to a paste "
"service and get a shareable URL.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""\
Examples:
hermes debug share Upload debug report and print URL
hermes debug share --lines 500 Include more log lines
hermes debug share --expire 30 Keep paste for 30 days
hermes debug share --local Print report locally (no upload)
hermes debug share --no-redact Disable upload-time secret redaction
hermes debug delete <url> Delete a previously uploaded paste
""",
)
debug_sub = debug_parser.add_subparsers(dest="debug_command")
share_parser = debug_sub.add_parser(
"share",
help="Upload debug report to a paste service and print a shareable URL",
)
share_parser.add_argument(
"--lines",
type=int,
default=200,
help="Number of log lines to include per log file (default: 200)",
)
share_parser.add_argument(
"--expire",
type=int,
default=7,
help="Paste expiry in days (default: 7)",
)
share_parser.add_argument(
"--local",
action="store_true",
help="Print the report locally instead of uploading",
)
share_parser.add_argument(
"--no-redact",
action="store_true",
help=(
"Disable upload-time secret redaction (default: redact). Logs "
"are normally run through agent.redact.redact_sensitive_text "
"with force=True before upload so credentials are not leaked "
"into the public paste service."
),
)
delete_parser = debug_sub.add_parser(
"delete",
help="Delete a paste uploaded by 'hermes debug share'",
)
delete_parser.add_argument(
"urls",
nargs="*",
default=[],
help="One or more paste URLs to delete (e.g. https://paste.rs/abc123)",
)
debug_parser.set_defaults(func=cmd_debug)
+35
View File
@@ -0,0 +1,35 @@
"""``hermes doctor`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_doctor_parser(subparsers, *, cmd_doctor: Callable) -> None:
"""Attach the ``doctor`` subcommand to ``subparsers``."""
# =========================================================================
# doctor command
# =========================================================================
doctor_parser = subparsers.add_parser(
"doctor",
help="Check configuration and dependencies",
description="Diagnose issues with Hermes Agent setup",
)
doctor_parser.add_argument(
"--fix", action="store_true", help="Attempt to fix issues automatically"
)
doctor_parser.add_argument(
"--ack",
metavar="ADVISORY_ID",
default=None,
help=(
"Acknowledge a security advisory by ID and exit. After ack, the "
"advisory will no longer trigger startup banners. Run `hermes "
"doctor` first to see active advisories and their IDs."
),
)
doctor_parser.set_defaults(func=cmd_doctor)
+28
View File
@@ -0,0 +1,28 @@
"""``hermes dump`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_dump_parser(subparsers, *, cmd_dump: Callable) -> None:
"""Attach the ``dump`` subcommand to ``subparsers``."""
# =========================================================================
# dump command
# =========================================================================
dump_parser = subparsers.add_parser(
"dump",
help="Dump setup summary for support/debugging",
description="Output a compact, plain-text summary of your Hermes setup "
"that can be copy-pasted into Discord/GitHub for support context",
)
dump_parser.add_argument(
"--show-keys",
action="store_true",
help="Show redacted API key prefixes (first/last 4 chars) instead of just set/not set",
)
dump_parser.set_defaults(func=cmd_dump)
+284
View File
@@ -0,0 +1,284 @@
"""``hermes gateway`` and ``hermes proxy`` subcommand parsers.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Both parsers are built together because they shared one inline block (the
``gateway`` section also defined ``proxy``). Handlers injected to avoid
importing ``main``.
"""
from __future__ import annotations
import argparse
from typing import Callable
from hermes_cli.subcommands._shared import add_accept_hooks_flag
def _add_compat_platform_flag(parser: argparse.ArgumentParser) -> None:
"""Accept stale `gateway <verb> --platform X` docs without advertising it.
Gateway service lifecycle commands operate on the gateway process, not a
single messaging adapter. Photon briefly printed a per-platform start
command during setup; keep that command parseable so users following the
old hint don't get blocked by argparse before the gateway can start.
"""
parser.add_argument(
"--platform",
dest="platform",
help=argparse.SUPPRESS,
)
def build_gateway_parser(subparsers, *, cmd_gateway: Callable, cmd_proxy: Callable) -> None:
"""Attach the ``gateway`` and ``proxy`` subcommands to ``subparsers``."""
# =========================================================================
# gateway command
# =========================================================================
gateway_parser = subparsers.add_parser(
"gateway",
help="Messaging gateway management",
description="Manage the messaging gateway (Telegram, Discord, WhatsApp, Weixin, and more)",
)
gateway_subparsers = gateway_parser.add_subparsers(dest="gateway_command")
# gateway run (default)
gateway_run = gateway_subparsers.add_parser(
"run", help="Run gateway in foreground (recommended for WSL, Docker, Termux)"
)
gateway_run.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Increase stderr log verbosity (-v=INFO, -vv=DEBUG)",
)
gateway_run.add_argument(
"-q", "--quiet", action="store_true", help="Suppress all stderr log output"
)
gateway_run.add_argument(
"--replace",
action="store_true",
help="Replace any existing gateway instance (useful for systemd)",
)
gateway_run.add_argument(
"--force",
action="store_true",
help=(
"Start a foreground gateway even when a systemd/launchd/s6 service "
"already supervises this profile. Without --force, the command "
"refuses because a second dispatcher escapes the service and can "
"corrupt shared gateway state."
),
)
gateway_run.add_argument(
"--no-supervise",
action="store_true",
help=(
"Inside the s6-overlay Docker image, normally `gateway run` is "
"automatically redirected to the supervised s6 service (so the "
"gateway gets auto-restart on crash, plus a supervised dashboard "
"if HERMES_DASHBOARD is set). Pass --no-supervise to opt out and "
"get the historical pre-s6 foreground behavior: the gateway is "
"the container's main process and the container exits with the "
"gateway's exit code. No effect outside an s6 container."
),
)
add_accept_hooks_flag(gateway_run)
add_accept_hooks_flag(gateway_parser)
# gateway start
gateway_start = gateway_subparsers.add_parser(
"start", help="Start the installed systemd/launchd background service"
)
gateway_start.add_argument(
"--system",
action="store_true",
help="Target the Linux system-level gateway service",
)
gateway_start.add_argument(
"--all",
action="store_true",
help="Kill ALL stale gateway processes across all profiles before starting",
)
_add_compat_platform_flag(gateway_start)
# gateway stop
gateway_stop = gateway_subparsers.add_parser("stop", help="Stop gateway service")
gateway_stop.add_argument(
"--system",
action="store_true",
help="Target the Linux system-level gateway service",
)
gateway_stop.add_argument(
"--all",
action="store_true",
help="Stop ALL gateway processes across all profiles",
)
# gateway restart
gateway_restart = gateway_subparsers.add_parser(
"restart", help="Restart gateway service"
)
gateway_restart.add_argument(
"--system",
action="store_true",
help="Target the Linux system-level gateway service",
)
gateway_restart.add_argument(
"--all",
action="store_true",
help="Kill ALL gateway processes across all profiles before restarting",
)
_add_compat_platform_flag(gateway_restart)
# gateway status
gateway_status = gateway_subparsers.add_parser("status", help="Show gateway status")
gateway_status.add_argument("--deep", action="store_true", help="Deep status check")
gateway_status.add_argument(
"-l",
"--full",
action="store_true",
help="Show full, untruncated service/log output where supported",
)
gateway_status.add_argument(
"--system",
action="store_true",
help="Target the Linux system-level gateway service",
)
_add_compat_platform_flag(gateway_status)
# gateway install
gateway_install = gateway_subparsers.add_parser(
"install", help="Install gateway as a systemd/launchd background service"
)
gateway_install.add_argument("--force", action="store_true", help="Force reinstall")
gateway_install.add_argument(
"--system",
action="store_true",
help="Install as a Linux system-level service (starts at boot)",
)
gateway_install.add_argument(
"--run-as-user",
dest="run_as_user",
help="User account the Linux system service should run as",
)
gateway_install.add_argument(
"--start-now",
dest="start_now",
action="store_true",
default=None,
help=argparse.SUPPRESS,
)
gateway_install.add_argument(
"--no-start-now",
dest="start_now",
action="store_false",
help=argparse.SUPPRESS,
)
gateway_install.add_argument(
"--start-on-login",
dest="start_on_login",
action="store_true",
default=None,
help=argparse.SUPPRESS,
)
gateway_install.add_argument(
"--no-start-on-login",
dest="start_on_login",
action="store_false",
help=argparse.SUPPRESS,
)
gateway_install.add_argument(
"--elevated-handoff",
dest="elevated_handoff",
action="store_true",
help=argparse.SUPPRESS,
)
# gateway uninstall
gateway_uninstall = gateway_subparsers.add_parser(
"uninstall", help="Uninstall gateway service"
)
gateway_uninstall.add_argument(
"--system",
action="store_true",
help="Target the Linux system-level gateway service",
)
# gateway list
gateway_subparsers.add_parser("list", help="List all profiles and their gateway status")
# gateway setup
gateway_subparsers.add_parser("setup", help="Configure messaging platforms")
# gateway migrate-legacy
gateway_migrate_legacy = gateway_subparsers.add_parser(
"migrate-legacy",
help="Remove legacy hermes.service units from pre-rename installs",
description=(
"Stop, disable, and remove legacy Hermes gateway unit files "
"(e.g. hermes.service) left over from older installs. Profile "
"units (hermes-gateway-<profile>.service) and unrelated "
"third-party services are never touched."
),
)
gateway_migrate_legacy.add_argument(
"--dry-run",
dest="dry_run",
action="store_true",
help="List what would be removed without doing it",
)
gateway_migrate_legacy.add_argument(
"-y",
"--yes",
dest="yes",
action="store_true",
help="Skip the confirmation prompt",
)
# =========================================================================
# proxy command — local OpenAI-compatible proxy that attaches the user's
# OAuth-authenticated provider credentials to outbound requests. Lets
# external apps (OpenViking, Karakeep, Open WebUI, ...) ride a logged-in
# subscription without copy-pasting static API keys.
# =========================================================================
proxy_parser = subparsers.add_parser(
"proxy",
help="Local OpenAI-compatible proxy to OAuth providers",
description=(
"Run a local HTTP server that forwards OpenAI-compatible requests "
"to an OAuth-authenticated provider (e.g. Nous Portal). External "
"apps can point at the proxy with any bearer token; the proxy "
"attaches your real credentials."
),
)
proxy_subparsers = proxy_parser.add_subparsers(dest="proxy_command")
proxy_start = proxy_subparsers.add_parser(
"start", help="Run the proxy in the foreground"
)
proxy_start.add_argument(
"--provider",
default="nous",
help="Upstream provider: nous or xai (default: nous). See `hermes proxy providers`.",
)
proxy_start.add_argument(
"--host",
default=None,
help="Bind address (default: 127.0.0.1). Use 0.0.0.0 to expose on LAN.",
)
proxy_start.add_argument(
"--port",
type=int,
default=None,
help="Bind port (default: 8645)",
)
proxy_subparsers.add_parser(
"status", help="Show which proxy upstreams are ready"
)
proxy_subparsers.add_parser(
"providers", help="List available proxy upstream providers"
)
proxy_parser.set_defaults(func=cmd_proxy)
gateway_parser.set_defaults(func=cmd_gateway)
+63
View File
@@ -0,0 +1,63 @@
"""``hermes gui`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_gui_parser(subparsers, *, cmd_gui: Callable) -> None:
"""Attach the ``gui`` subcommand to ``subparsers``."""
# =========================================================================
gui_parser = subparsers.add_parser(
"desktop",
aliases=["gui"],
help="Build and launch the native desktop app",
description=(
"Launch the Hermes Electron desktop app. By default this installs "
"workspace Node dependencies, builds the current OS's unpacked "
"Electron app, then launches that packaged artifact."
),
)
gui_parser.add_argument(
"--source",
action="store_true",
help="Launch via `electron .` against apps/desktop/dist instead of the packaged app",
)
gui_parser.add_argument(
"--build-only",
action="store_true",
help="Build the desktop app but do not launch it (used by the installer's --update flow)",
)
gui_parser.add_argument(
"--fake-boot",
action="store_true",
help="Enable deterministic desktop boot delays for validating startup UI",
)
gui_parser.add_argument(
"--ignore-existing",
action="store_true",
help="Force Desktop to ignore any hermes CLI already on PATH during backend resolution",
)
gui_parser.add_argument(
"--hermes-root",
help="Override the Hermes source root used by Desktop (sets HERMES_DESKTOP_HERMES_ROOT)",
)
gui_parser.add_argument(
"--cwd",
help="Initial project directory for Desktop chat sessions (sets HERMES_DESKTOP_CWD)",
)
gui_parser.add_argument(
"--skip-build",
action="store_true",
help="Skip npm install/package and launch the existing unpacked app from apps/desktop/release",
)
gui_parser.add_argument(
"--force-build",
action="store_true",
help="Force a full rebuild even if the content stamp matches",
)
gui_parser.set_defaults(func=cmd_gui)
+77
View File
@@ -0,0 +1,77 @@
"""``hermes hooks`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_hooks_parser(subparsers, *, cmd_hooks: Callable) -> None:
"""Attach the ``hooks`` subcommand to ``subparsers``."""
# =========================================================================
hooks_parser = subparsers.add_parser(
"hooks",
help="Inspect and manage shell-script hooks",
description=(
"Inspect shell-script hooks declared in ~/.hermes/config.yaml, "
"test them against synthetic payloads, and manage the first-use "
"consent allowlist at ~/.hermes/shell-hooks-allowlist.json."
),
)
hooks_subparsers = hooks_parser.add_subparsers(dest="hooks_action")
hooks_subparsers.add_parser(
"list",
aliases=["ls"],
help="List configured hooks with matcher, timeout, and consent status",
)
_hk_test = hooks_subparsers.add_parser(
"test",
help="Fire every hook matching <event> against a synthetic payload",
)
_hk_test.add_argument(
"event",
help="Hook event name (e.g. pre_tool_call, pre_llm_call, subagent_stop)",
)
_hk_test.add_argument(
"--for-tool",
dest="for_tool",
default=None,
help=(
"Only fire hooks whose matcher matches this tool name "
"(used for pre_tool_call / post_tool_call)"
),
)
_hk_test.add_argument(
"--payload-file",
dest="payload_file",
default=None,
help=(
"Path to a JSON file whose contents are merged into the "
"synthetic payload before execution"
),
)
_hk_revoke = hooks_subparsers.add_parser(
"revoke",
aliases=["remove", "rm"],
help="Remove a command's allowlist entries (takes effect on next restart)",
)
_hk_revoke.add_argument(
"command",
help="The exact command string to revoke (as declared in config.yaml)",
)
hooks_subparsers.add_parser(
"doctor",
help=(
"Check each configured hook: exec bit, allowlist, mtime drift, "
"JSON validity, and synthetic run timing"
),
)
hooks_parser.set_defaults(func=cmd_hooks)
+31
View File
@@ -0,0 +1,31 @@
"""``hermes import`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_import_cmd_parser(subparsers, *, cmd_import: Callable) -> None:
"""Attach the ``import`` subcommand to ``subparsers``."""
# =========================================================================
# import command
# =========================================================================
import_parser = subparsers.add_parser(
"import",
help="Restore a Hermes backup from a zip file",
description="Extract a previously created Hermes backup into your "
"Hermes home directory, restoring configuration, skills, "
"sessions, and data",
)
import_parser.add_argument("zipfile", help="Path to the backup zip file")
import_parser.add_argument(
"--force",
"-f",
action="store_true",
help="Overwrite existing files without confirmation",
)
import_parser.set_defaults(func=cmd_import)
+25
View 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)
+58
View File
@@ -0,0 +1,58 @@
"""``hermes login`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_login_parser(subparsers, *, cmd_login: Callable) -> None:
"""Attach the ``login`` subcommand to ``subparsers``."""
# =========================================================================
# login command
# =========================================================================
login_parser = subparsers.add_parser(
"login",
help="Authenticate with an inference provider",
description="Run OAuth device authorization flow for Hermes CLI",
)
login_parser.add_argument(
"--provider",
choices=["nous", "openai-codex", "xai-oauth"],
default=None,
help="Provider to authenticate with (default: nous)",
)
login_parser.add_argument(
"--portal-url", help="Portal base URL (default: production portal)"
)
login_parser.add_argument(
"--inference-url",
help="Inference API base URL (default: production inference API)",
)
login_parser.add_argument(
"--client-id", default=None, help="OAuth client id to use (default: hermes-cli)"
)
login_parser.add_argument("--scope", default=None, help="OAuth scope to request")
login_parser.add_argument(
"--no-browser",
action="store_true",
help="Do not attempt to open the browser automatically",
)
login_parser.add_argument(
"--timeout",
type=float,
default=15.0,
help="HTTP request timeout in seconds (default: 15)",
)
login_parser.add_argument(
"--ca-bundle", help="Path to CA bundle PEM file for TLS verification"
)
login_parser.add_argument(
"--insecure",
action="store_true",
help="Disable TLS verification (testing only)",
)
login_parser.set_defaults(func=cmd_login)
+28
View File
@@ -0,0 +1,28 @@
"""``hermes logout`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_logout_parser(subparsers, *, cmd_logout: Callable) -> None:
"""Attach the ``logout`` subcommand to ``subparsers``."""
# =========================================================================
# logout command
# =========================================================================
logout_parser = subparsers.add_parser(
"logout",
help="Clear authentication for an inference provider",
description="Remove stored credentials and reset provider config",
)
logout_parser.add_argument(
"--provider",
choices=["nous", "openai-codex", "xai-oauth", "spotify"],
default=None,
help="Provider to log out from (default: active provider)",
)
logout_parser.set_defaults(func=cmd_logout)
+78
View File
@@ -0,0 +1,78 @@
"""``hermes logs`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
import argparse
from typing import Callable
def build_logs_parser(subparsers, *, cmd_logs: Callable) -> None:
"""Attach the ``logs`` subcommand to ``subparsers``."""
# =========================================================================
# logs command
# =========================================================================
logs_parser = subparsers.add_parser(
"logs",
help="View and filter Hermes log files",
description="View, tail, and filter agent.log / errors.log / gateway.log / gui.log / desktop.log",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""\
Examples:
hermes logs Show last 50 lines of agent.log
hermes logs -f Follow agent.log in real time
hermes logs errors Show last 50 lines of errors.log
hermes logs gateway -n 100 Show last 100 lines of gateway.log
hermes logs gui -f Follow gui.log in real time
hermes logs desktop -f Follow desktop.log (Electron app boot/backend)
hermes logs --level WARNING Only show WARNING and above
hermes logs --session abc123 Filter by session ID
hermes logs --component tools Only show tool-related lines
hermes logs --since 1h Lines from the last hour
hermes logs --since 30m -f Follow, starting from 30 min ago
hermes logs list List available log files with sizes
""",
)
logs_parser.add_argument(
"log_name",
nargs="?",
default="agent",
help="Log to view: agent (default), errors, gateway, gui, or 'list' to show available files",
)
logs_parser.add_argument(
"-n",
"--lines",
type=int,
default=50,
help="Number of lines to show (default: 50)",
)
logs_parser.add_argument(
"-f",
"--follow",
action="store_true",
help="Follow the log in real time (like tail -f)",
)
logs_parser.add_argument(
"--level",
metavar="LEVEL",
help="Minimum log level to show (DEBUG, INFO, WARNING, ERROR)",
)
logs_parser.add_argument(
"--session",
metavar="ID",
help="Filter lines containing this session ID substring",
)
logs_parser.add_argument(
"--since",
metavar="TIME",
help="Show lines since TIME ago (e.g. 1h, 30m, 2d)",
)
logs_parser.add_argument(
"--component",
metavar="NAME",
help="Filter by component: gateway, agent, tools, cli, cron, gui",
)
logs_parser.set_defaults(func=cmd_logs)
+108
View File
@@ -0,0 +1,108 @@
"""``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
import argparse
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=argparse.REMAINDER,
default=[],
help="Arguments for stdio command; must be the last option",
)
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
View 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)
+72
View File
@@ -0,0 +1,72 @@
"""``hermes model`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_model_parser(subparsers, *, cmd_model: Callable) -> None:
"""Attach the ``model`` subcommand to ``subparsers``."""
# =========================================================================
# model command
# =========================================================================
model_parser = subparsers.add_parser(
"model",
help="Select default model and provider",
description="Interactively select your inference provider and default model",
)
model_parser.add_argument(
"--refresh",
action="store_true",
help="Wipe the model picker disk cache and re-fetch every provider's live /v1/models list.",
)
model_parser.add_argument(
"--portal-url",
help="Portal base URL for Nous login (default: production portal)",
)
model_parser.add_argument(
"--inference-url",
help="Inference API base URL for Nous login (default: production inference API)",
)
model_parser.add_argument(
"--client-id",
default=None,
help="OAuth client id to use for Nous login (default: hermes-cli)",
)
model_parser.add_argument(
"--scope", default=None, help="OAuth scope to request for Nous login"
)
model_parser.add_argument(
"--no-browser",
action="store_true",
help="Do not attempt to open the browser automatically during Nous login",
)
model_parser.add_argument(
"--manual-paste",
action="store_true",
help=(
"For loopback OAuth providers (xai-oauth, ...): skip the local "
"callback listener and paste the failed callback URL from your "
"browser instead. Use on browser-only remotes (Cloud Shell, "
"Codespaces, EC2 Instance Connect, ...). See #26923."
),
)
model_parser.add_argument(
"--timeout",
type=float,
default=15.0,
help="HTTP request timeout in seconds for Nous login (default: 15)",
)
model_parser.add_argument(
"--ca-bundle", help="Path to CA bundle PEM file for Nous TLS verification"
)
model_parser.add_argument(
"--insecure",
action="store_true",
help="Disable TLS verification for Nous login (testing only)",
)
model_parser.set_defaults(func=cmd_model)
+36
View 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
View 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)
+23
View File
@@ -0,0 +1,23 @@
"""``hermes postinstall`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_postinstall_parser(subparsers, *, cmd_postinstall: Callable) -> None:
"""Attach the ``postinstall`` subcommand to ``subparsers``."""
# =========================================================================
# postinstall command
# =========================================================================
postinstall_parser = subparsers.add_parser(
"postinstall",
help="Bootstrap non-Python deps for pip installs (node, browser, ripgrep, ffmpeg)",
description="One-shot post-install for pip users. Installs system "
"dependencies that pip cannot provide, then runs setup if needed.",
)
postinstall_parser.set_defaults(func=cmd_postinstall)
+203
View File
@@ -0,0 +1,203 @@
"""``hermes profile`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None:
"""Attach the ``profile`` subcommand to ``subparsers``."""
# =========================================================================
# profile command
# =========================================================================
profile_parser = subparsers.add_parser(
"profile",
help="Manage profiles — multiple isolated Hermes instances",
)
profile_subparsers = profile_parser.add_subparsers(dest="profile_action")
profile_subparsers.add_parser("list", help="List all profiles")
profile_use = profile_subparsers.add_parser(
"use", help="Set sticky default profile"
)
profile_use.add_argument("profile_name", help="Profile name (or 'default')")
profile_create = profile_subparsers.add_parser(
"create", help="Create a new profile"
)
profile_create.add_argument(
"profile_name", help="Profile name (lowercase, alphanumeric)"
)
profile_create.add_argument(
"--clone",
action="store_true",
help="Copy config.yaml, .env, SOUL.md, and skills from active profile",
)
profile_create.add_argument(
"--clone-all",
action="store_true",
help="Full copy of active profile (all state, excluding per-profile history)",
)
profile_create.add_argument(
"--clone-from",
metavar="SOURCE",
help="Source profile to clone from; implies --clone unless --clone-all is set",
)
profile_create.add_argument(
"--no-alias", action="store_true", help="Skip wrapper script creation"
)
profile_create.add_argument(
"--no-skills",
action="store_true",
help="Create an empty profile with no bundled skills (opts out of `hermes update` skill sync)",
)
profile_create.add_argument(
"--description",
default=None,
help="One- or two-sentence description of what this profile is good at. "
"Used by the kanban decomposer to route tasks based on role instead "
"of profile name alone. Skip and add later via `hermes profile describe`.",
)
profile_delete = profile_subparsers.add_parser("delete", help="Delete a profile")
profile_delete.add_argument("profile_name", help="Profile to delete")
profile_delete.add_argument(
"-y", "--yes", action="store_true", help="Skip confirmation prompt"
)
profile_describe = profile_subparsers.add_parser(
"describe",
help="Read or set a profile's description (used by the kanban orchestrator)",
)
profile_describe.add_argument(
"profile_name",
nargs="?",
default=None,
help="Profile to describe (omit + use --all --auto to sweep)",
)
profile_describe.add_argument(
"--text",
default=None,
help="Set description to this exact text (overwrites any existing description)",
)
profile_describe.add_argument(
"--auto",
action="store_true",
help="Auto-generate description via the auxiliary LLM "
"(uses auxiliary.profile_describer)",
)
profile_describe.add_argument(
"--overwrite",
action="store_true",
help="With --auto, replace user-authored descriptions too (default: only "
"fill in missing or previously-auto descriptions)",
)
profile_describe.add_argument(
"--all",
dest="all_missing",
action="store_true",
help="With --auto, run on every profile missing a description",
)
profile_show = profile_subparsers.add_parser("show", help="Show profile details")
profile_show.add_argument("profile_name", help="Profile to show")
profile_alias = profile_subparsers.add_parser(
"alias", help="Manage wrapper scripts"
)
profile_alias.add_argument("profile_name", help="Profile name")
profile_alias.add_argument(
"--remove", action="store_true", help="Remove the wrapper script"
)
profile_alias.add_argument(
"--name",
dest="alias_name",
metavar="NAME",
help="Custom alias name (default: profile name)",
)
profile_rename = profile_subparsers.add_parser("rename", help="Rename a profile")
profile_rename.add_argument("old_name", help="Current profile name")
profile_rename.add_argument("new_name", help="New profile name")
profile_export = profile_subparsers.add_parser(
"export", help="Export a profile to archive"
)
profile_export.add_argument("profile_name", help="Profile to export")
profile_export.add_argument(
"-o", "--output", default=None, help="Output file (default: <name>.tar.gz)"
)
profile_import = profile_subparsers.add_parser(
"import", help="Import a profile from archive"
)
profile_import.add_argument("archive", help="Path to .tar.gz archive")
profile_import.add_argument(
"--name",
dest="import_name",
metavar="NAME",
help="Profile name (default: inferred from archive)",
)
# ---------- Distribution subcommands (issue #20456) ----------
profile_install = profile_subparsers.add_parser(
"install",
help="Install a profile distribution from a git URL or local directory",
description=(
"Install a Hermes profile distribution. SOURCE can be a git URL "
"(github.com/user/repo, https://..., git@...) or a local "
"directory containing distribution.yaml at its root."
),
)
profile_install.add_argument(
"source",
help="Distribution source (git URL or local directory)",
)
profile_install.add_argument(
"--name", dest="install_name", metavar="NAME",
help="Override profile name (default: read from manifest)",
)
profile_install.add_argument(
"--alias", action="store_true",
help="Create a shell wrapper alias for the installed profile",
)
profile_install.add_argument(
"--force", action="store_true",
help="Overwrite an existing profile of the same name (user data preserved)",
)
profile_install.add_argument(
"-y", "--yes", action="store_true",
help="Skip manifest preview confirmation",
)
profile_update = profile_subparsers.add_parser(
"update",
help="Re-pull a distribution and apply updates (user data preserved)",
description=(
"Fetch the distribution from its recorded source and overwrite "
"distribution-owned files (SOUL.md, skills/, cron/, mcp.json). "
"User data (memories, sessions, auth, .env) is never touched. "
"config.yaml is preserved unless --force-config is passed."
),
)
profile_update.add_argument("profile_name", help="Profile to update")
profile_update.add_argument(
"--force-config", action="store_true",
help="Also overwrite config.yaml (normally preserved to keep user overrides)",
)
profile_update.add_argument(
"-y", "--yes", action="store_true",
help="Skip confirmation",
)
profile_info = profile_subparsers.add_parser(
"info",
help="Show a profile's distribution manifest (version, requirements, source)",
)
profile_info.add_argument("profile_name", help="Profile to inspect")
profile_parser.set_defaults(func=cmd_profile)
+36
View File
@@ -0,0 +1,36 @@
"""``hermes prompt-size`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_prompt_size_parser(subparsers, *, cmd_prompt_size: Callable) -> None:
"""Attach the ``prompt-size`` subcommand to ``subparsers``."""
# =========================================================================
# prompt-size command
# =========================================================================
prompt_size_parser = subparsers.add_parser(
"prompt-size",
help="Show a byte breakdown of the system prompt + tool schemas",
description=(
"Report the fixed prompt budget for a fresh session: system "
"prompt total, skills index, memory, user profile, and tool-schema "
"JSON. Runs offline (no API call)."
),
)
prompt_size_parser.add_argument(
"--platform",
default="cli",
help="Platform to simulate (cli, telegram, discord, ...). Default: cli",
)
prompt_size_parser.add_argument(
"--json",
action="store_true",
help="Emit the breakdown as JSON",
)
prompt_size_parser.set_defaults(func=cmd_prompt_size)
+62
View File
@@ -0,0 +1,62 @@
"""``hermes security`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_security_parser(subparsers, *, cmd_security: Callable) -> None:
"""Attach the ``security`` subcommand to ``subparsers``."""
# =========================================================================
security_parser = subparsers.add_parser(
"security",
help="Supply-chain audit (OSV.dev) for venv, plugins, and MCP servers",
description=(
"On-demand vulnerability scan against OSV.dev. Covers the Hermes "
"venv (installed PyPI dists), Python deps declared by plugins under "
"~/.hermes/plugins/, and pinned npx/uvx MCP servers in config.yaml. "
"Does NOT scan globally-installed packages or editor/browser extensions."
),
)
security_subparsers = security_parser.add_subparsers(
dest="security_command",
metavar="<subcommand>",
)
audit_parser = security_subparsers.add_parser(
"audit",
help="Run a one-shot supply-chain audit",
description="Query OSV.dev for known vulnerabilities in installed components.",
)
audit_parser.add_argument(
"--json",
action="store_true",
help="Emit machine-readable JSON instead of human-readable text",
)
audit_parser.add_argument(
"--fail-on",
default="critical",
choices=["low", "moderate", "high", "critical"],
help="Exit non-zero when any finding meets this severity (default: critical)",
)
audit_parser.add_argument(
"--skip-venv",
action="store_true",
help="Skip scanning the Hermes Python venv",
)
audit_parser.add_argument(
"--skip-plugins",
action="store_true",
help="Skip scanning plugin requirements files",
)
audit_parser.add_argument(
"--skip-mcp",
action="store_true",
help="Skip scanning pinned MCP servers in config.yaml",
)
audit_parser.set_defaults(func=cmd_security)
security_parser.set_defaults(func=cmd_security)
+58
View File
@@ -0,0 +1,58 @@
"""``hermes setup`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_setup_parser(subparsers, *, cmd_setup: Callable) -> None:
"""Attach the ``setup`` subcommand to ``subparsers``."""
# =========================================================================
# setup command
# =========================================================================
setup_parser = subparsers.add_parser(
"setup",
help="Interactive setup wizard",
description="Configure Hermes Agent with an interactive wizard. "
"Run a specific section: hermes setup model|tts|terminal|gateway|tools|agent",
)
setup_parser.add_argument(
"section",
nargs="?",
choices=["model", "tts", "terminal", "gateway", "tools", "agent"],
default=None,
help="Run a specific setup section instead of the full wizard",
)
setup_parser.add_argument(
"--non-interactive",
action="store_true",
help="Non-interactive mode (use defaults/env vars)",
)
setup_parser.add_argument(
"--reset", action="store_true", help="Reset configuration to defaults"
)
setup_parser.add_argument(
"--reconfigure",
action="store_true",
help="(Default on existing installs.) Re-run the full wizard, "
"showing current values as defaults. Kept for backwards "
"compatibility — a bare 'hermes setup' now does this.",
)
setup_parser.add_argument(
"--quick",
action="store_true",
help="On existing installs: only prompt for items that are missing "
"or unset, instead of running the full reconfigure wizard.",
)
setup_parser.add_argument(
"--portal",
action="store_true",
help="One-shot Nous Portal setup: log in via OAuth, pick a Nous "
"model, set Nous as the inference provider, and opt into the Tool "
"Gateway. Skips the rest of the wizard.",
)
setup_parser.set_defaults(func=cmd_setup)
+269
View 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)
+60
View File
@@ -0,0 +1,60 @@
"""``hermes slack`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_slack_parser(subparsers, *, cmd_slack: Callable) -> None:
"""Attach the ``slack`` subcommand to ``subparsers``."""
# =========================================================================
# slack command
# =========================================================================
slack_parser = subparsers.add_parser(
"slack",
help="Slack integration helpers (manifest generation, etc.)",
description="Slack integration helpers for Hermes.",
)
slack_sub = slack_parser.add_subparsers(dest="slack_command")
slack_manifest = slack_sub.add_parser(
"manifest",
help="Print or write a Slack app manifest with every gateway command "
"registered as a native slash (/btw, /stop, /model, ...)",
description=(
"Generate a Slack app manifest that registers every gateway "
"command in COMMAND_REGISTRY as a first-class Slack slash "
"command (matching Discord and Telegram parity). Paste the "
"output into Slack app config → Features → App Manifest → "
"Edit, then Save. Reinstall the app if Slack prompts for it."
),
)
slack_manifest.add_argument(
"--write",
nargs="?",
const=True,
default=None,
metavar="PATH",
help="Write manifest to a file instead of stdout. With no PATH "
"writes to $HERMES_HOME/slack-manifest.json.",
)
slack_manifest.add_argument(
"--name",
default=None,
help='Bot display name (default: "Hermes")',
)
slack_manifest.add_argument(
"--description",
default=None,
help="Bot description shown in Slack's app directory.",
)
slack_manifest.add_argument(
"--slashes-only",
action="store_true",
help="Emit only the features.slash_commands array (for merging "
"into an existing manifest manually).",
)
slack_parser.set_defaults(func=cmd_slack)
+28
View File
@@ -0,0 +1,28 @@
"""``hermes status`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_status_parser(subparsers, *, cmd_status: Callable) -> None:
"""Attach the ``status`` subcommand to ``subparsers``."""
# =========================================================================
# status command
# =========================================================================
status_parser = subparsers.add_parser(
"status",
help="Show status of all components",
description="Display status of Hermes Agent components",
)
status_parser.add_argument(
"--all", action="store_true", help="Show all details (redacted for sharing)"
)
status_parser.add_argument(
"--deep", action="store_true", help="Run deep checks (may take longer)"
)
status_parser.set_defaults(func=cmd_status)
+95
View 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)
+41
View File
@@ -0,0 +1,41 @@
"""``hermes uninstall`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_uninstall_parser(subparsers, *, cmd_uninstall: Callable) -> None:
"""Attach the ``uninstall`` subcommand to ``subparsers``."""
# =========================================================================
# uninstall command
# =========================================================================
uninstall_parser = subparsers.add_parser(
"uninstall",
help="Uninstall Hermes Agent",
description="Remove Hermes Agent from your system. Can keep configs/data for reinstall.",
)
uninstall_parser.add_argument(
"--full",
action="store_true",
help="Full uninstall - remove everything including configs and data",
)
uninstall_parser.add_argument(
"--gui",
action="store_true",
help="Uninstall only the desktop Chat GUI, leaving the agent intact",
)
uninstall_parser.add_argument(
"--gui-summary",
action="store_true",
help="Print a JSON summary of installed GUI/agent artifacts and exit "
"(used by the desktop app to gate uninstall options)",
)
uninstall_parser.add_argument(
"--yes", "-y", action="store_true", help="Skip confirmation prompts"
)
uninstall_parser.set_defaults(func=cmd_uninstall)
+70
View File
@@ -0,0 +1,70 @@
"""``hermes update`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_update_parser(subparsers, *, cmd_update: Callable) -> None:
"""Attach the ``update`` subcommand to ``subparsers``."""
# =========================================================================
# update command
# =========================================================================
update_parser = subparsers.add_parser(
"update",
help="Update Hermes Agent to the latest version",
description="Pull the latest changes from git and reinstall dependencies",
)
update_parser.add_argument(
"--gateway",
action="store_true",
default=False,
help="Gateway mode: use file-based IPC for prompts instead of stdin (used internally by /update)",
)
update_parser.add_argument(
"--check",
action="store_true",
default=False,
help="Check whether an update is available without installing anything",
)
update_parser.add_argument(
"--no-backup",
action="store_true",
default=False,
help="Skip the pre-update backup for this run (overrides updates.pre_update_backup)",
)
update_parser.add_argument(
"--backup",
action="store_true",
default=False,
help="Force a pre-update backup for this run (off by default; overrides updates.pre_update_backup)",
)
update_parser.add_argument(
"--yes",
"-y",
action="store_true",
default=False,
help="Assume yes for interactive prompts (config migration, stash restore). API-key entry is skipped; run 'hermes config migrate' separately for those.",
)
update_parser.add_argument(
"--branch",
default=None,
metavar="NAME",
help=(
"Update against this branch instead of the default (main). "
"If the local checkout is on a different branch, hermes will "
"switch to the requested branch first (auto-stashing any "
"uncommitted changes)."
),
)
update_parser.add_argument(
"--force",
action="store_true",
default=False,
help="Windows: proceed with the update even when another hermes.exe is detected. The concurrent process will likely cause WinError 32 warnings and may leave a reboot-deferred .exe replacement.",
)
update_parser.set_defaults(func=cmd_update)
+18
View File
@@ -0,0 +1,18 @@
"""``hermes version`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_version_parser(subparsers, *, cmd_version: Callable) -> None:
"""Attach the ``version`` subcommand to ``subparsers``."""
# =========================================================================
# version command
# =========================================================================
version_parser = subparsers.add_parser("version", help="Show version information")
version_parser.set_defaults(func=cmd_version)
+76
View File
@@ -0,0 +1,76 @@
"""``hermes webhook`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_webhook_parser(subparsers, *, cmd_webhook: Callable) -> None:
"""Attach the ``webhook`` subcommand to ``subparsers``."""
# =========================================================================
# webhook command
# =========================================================================
webhook_parser = subparsers.add_parser(
"webhook",
help="Manage dynamic webhook subscriptions",
description="Create, list, and remove webhook subscriptions for event-driven agent activation",
)
webhook_subparsers = webhook_parser.add_subparsers(dest="webhook_action")
wh_sub = webhook_subparsers.add_parser(
"subscribe", aliases=["add"], help="Create a webhook subscription"
)
wh_sub.add_argument("name", help="Route name (used in URL: /webhooks/<name>)")
wh_sub.add_argument(
"--prompt", default="", help="Prompt template with {dot.notation} payload refs"
)
wh_sub.add_argument(
"--events", default="", help="Comma-separated event types to accept"
)
wh_sub.add_argument("--description", default="", help="What this subscription does")
wh_sub.add_argument(
"--skills", default="", help="Comma-separated skill names to load"
)
wh_sub.add_argument(
"--deliver",
default="log",
help="Delivery target: log, telegram, discord, slack, etc.",
)
wh_sub.add_argument(
"--deliver-chat-id",
default="",
help="Target chat ID for cross-platform delivery",
)
wh_sub.add_argument(
"--secret", default="", help="HMAC secret (auto-generated if omitted)"
)
wh_sub.add_argument(
"--deliver-only",
action="store_true",
help="Skip the agent — deliver the rendered prompt directly as the "
"message. Zero LLM cost. Requires --deliver to be a real target "
"(not 'log').",
)
webhook_subparsers.add_parser(
"list", aliases=["ls"], help="List all dynamic subscriptions"
)
wh_rm = webhook_subparsers.add_parser(
"remove", aliases=["rm"], help="Remove a subscription"
)
wh_rm.add_argument("name", help="Subscription name to remove")
wh_test = webhook_subparsers.add_parser(
"test", help="Send a test POST to a webhook route"
)
wh_test.add_argument("name", help="Subscription name to test")
wh_test.add_argument(
"--payload", default="", help="JSON payload to send (default: test payload)"
)
webhook_parser.set_defaults(func=cmd_webhook)
+22
View File
@@ -0,0 +1,22 @@
"""``hermes whatsapp`` subcommand parser.
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
Handler injected to avoid importing ``main``.
"""
from __future__ import annotations
from typing import Callable
def build_whatsapp_parser(subparsers, *, cmd_whatsapp: Callable) -> None:
"""Attach the ``whatsapp`` subcommand to ``subparsers``."""
# =========================================================================
# whatsapp command
# =========================================================================
whatsapp_parser = subparsers.add_parser(
"whatsapp",
help="Set up WhatsApp integration",
description="Configure WhatsApp and pair via QR code",
)
whatsapp_parser.set_defaults(func=cmd_whatsapp)