mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
refactor(cli): extract 25 more subcommand parsers into hermes_cli/subcommands/
Batch extraction of every remaining subcommand whose handler is top-level and whose parser block is pure argparse: model, setup, postinstall, whatsapp, slack, login, logout, auth, status, webhook, hooks, doctor, security, dump, debug, backup, import, config, version, update, uninstall, dashboard, gui, logs, prompt-size. Each becomes hermes_cli/subcommands/<name>.py with build_<name>_parser() and an injected handler (no main import). dashboard also injects cmd_dashboard_register for its nested 'register' action. Behavior-neutral: all 25 subcommands' --help output (and nested subaction help) diff-verified byte-identical to pre-extraction. Two RawDescriptionHelpFormatter epilogs (debug, logs) needed their multi-line string interiors preserved at column 0 — caught by the --help diff, not compile. main() 3297 -> 1798 LOC across this PR; add_parser calls in main.py 179 -> 89. Validation: tests/hermes_cli/ 6476 passed / 0 failed under per-file process isolation; new test_subcommands_batch.py smoke-tests all 25 builders + the dashboard two-handler case.
This commit is contained in:
parent
4da45e8727
commit
568e127612
27 changed files with 1541 additions and 984 deletions
1068
hermes_cli/main.py
1068
hermes_cli/main.py
File diff suppressed because it is too large
Load diff
109
hermes_cli/subcommands/auth.py
Normal file
109
hermes_cli/subcommands/auth.py
Normal 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
hermes_cli/subcommands/backup.py
Normal file
38
hermes_cli/subcommands/backup.py
Normal 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)
|
||||
49
hermes_cli/subcommands/config.py
Normal file
49
hermes_cli/subcommands/config.py
Normal 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)
|
||||
123
hermes_cli/subcommands/dashboard.py
Normal file
123
hermes_cli/subcommands/dashboard.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""``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)"
|
||||
)
|
||||
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"
|
||||
),
|
||||
)
|
||||
# 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
hermes_cli/subcommands/debug.py
Normal file
77
hermes_cli/subcommands/debug.py
Normal 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
hermes_cli/subcommands/doctor.py
Normal file
35
hermes_cli/subcommands/doctor.py
Normal 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
hermes_cli/subcommands/dump.py
Normal file
28
hermes_cli/subcommands/dump.py
Normal 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)
|
||||
63
hermes_cli/subcommands/gui.py
Normal file
63
hermes_cli/subcommands/gui.py
Normal 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
hermes_cli/subcommands/hooks.py
Normal file
77
hermes_cli/subcommands/hooks.py
Normal 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
hermes_cli/subcommands/import_cmd.py
Normal file
31
hermes_cli/subcommands/import_cmd.py
Normal 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)
|
||||
58
hermes_cli/subcommands/login.py
Normal file
58
hermes_cli/subcommands/login.py
Normal 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
hermes_cli/subcommands/logout.py
Normal file
28
hermes_cli/subcommands/logout.py
Normal 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
hermes_cli/subcommands/logs.py
Normal file
78
hermes_cli/subcommands/logs.py
Normal 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)
|
||||
72
hermes_cli/subcommands/model.py
Normal file
72
hermes_cli/subcommands/model.py
Normal 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)
|
||||
23
hermes_cli/subcommands/postinstall.py
Normal file
23
hermes_cli/subcommands/postinstall.py
Normal 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)
|
||||
36
hermes_cli/subcommands/prompt_size.py
Normal file
36
hermes_cli/subcommands/prompt_size.py
Normal 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
hermes_cli/subcommands/security.py
Normal file
62
hermes_cli/subcommands/security.py
Normal 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
hermes_cli/subcommands/setup.py
Normal file
58
hermes_cli/subcommands/setup.py
Normal 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)
|
||||
60
hermes_cli/subcommands/slack.py
Normal file
60
hermes_cli/subcommands/slack.py
Normal 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
hermes_cli/subcommands/status.py
Normal file
28
hermes_cli/subcommands/status.py
Normal 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)
|
||||
41
hermes_cli/subcommands/uninstall.py
Normal file
41
hermes_cli/subcommands/uninstall.py
Normal 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
hermes_cli/subcommands/update.py
Normal file
70
hermes_cli/subcommands/update.py
Normal 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
hermes_cli/subcommands/version.py
Normal file
18
hermes_cli/subcommands/version.py
Normal 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
hermes_cli/subcommands/webhook.py
Normal file
76
hermes_cli/subcommands/webhook.py
Normal 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
hermes_cli/subcommands/whatsapp.py
Normal file
22
hermes_cli/subcommands/whatsapp.py
Normal 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)
|
||||
97
tests/hermes_cli/test_subcommands_batch.py
Normal file
97
tests/hermes_cli/test_subcommands_batch.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Smoke tests for the batch-extracted subcommand parser builders.
|
||||
|
||||
Each ``build_<group>_parser`` should attach its subcommand to a subparsers
|
||||
group and wire ``func`` to the injected handler. These are intentionally
|
||||
light — the byte-identical ``--help`` verification done at extraction time is
|
||||
the real behavioral guarantee; this just guards against a module failing to
|
||||
import or a builder raising.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.subcommands.auth import build_auth_parser
|
||||
from hermes_cli.subcommands.backup import build_backup_parser
|
||||
from hermes_cli.subcommands.config import build_config_parser
|
||||
from hermes_cli.subcommands.dashboard import build_dashboard_parser
|
||||
from hermes_cli.subcommands.debug import build_debug_parser
|
||||
from hermes_cli.subcommands.doctor import build_doctor_parser
|
||||
from hermes_cli.subcommands.dump import build_dump_parser
|
||||
from hermes_cli.subcommands.gui import build_gui_parser
|
||||
from hermes_cli.subcommands.hooks import build_hooks_parser
|
||||
from hermes_cli.subcommands.import_cmd import build_import_cmd_parser
|
||||
from hermes_cli.subcommands.login import build_login_parser
|
||||
from hermes_cli.subcommands.logout import build_logout_parser
|
||||
from hermes_cli.subcommands.logs import build_logs_parser
|
||||
from hermes_cli.subcommands.model import build_model_parser
|
||||
from hermes_cli.subcommands.postinstall import build_postinstall_parser
|
||||
from hermes_cli.subcommands.prompt_size import build_prompt_size_parser
|
||||
from hermes_cli.subcommands.security import build_security_parser
|
||||
from hermes_cli.subcommands.setup import build_setup_parser
|
||||
from hermes_cli.subcommands.slack import build_slack_parser
|
||||
from hermes_cli.subcommands.status import build_status_parser
|
||||
from hermes_cli.subcommands.uninstall import build_uninstall_parser
|
||||
from hermes_cli.subcommands.update import build_update_parser
|
||||
from hermes_cli.subcommands.version import build_version_parser
|
||||
from hermes_cli.subcommands.webhook import build_webhook_parser
|
||||
from hermes_cli.subcommands.whatsapp import build_whatsapp_parser
|
||||
|
||||
|
||||
def _h(name):
|
||||
def handler(args): # pragma: no cover - identity only
|
||||
return name
|
||||
handler.__name__ = f"cmd_{name}"
|
||||
return handler
|
||||
|
||||
|
||||
# (subcommand_name, builder, handler_kwargs, sample_argv)
|
||||
SINGLE_HANDLER_CASES = [
|
||||
("model", build_model_parser, "cmd_model", ["model"]),
|
||||
("setup", build_setup_parser, "cmd_setup", ["setup"]),
|
||||
("postinstall", build_postinstall_parser, "cmd_postinstall", ["postinstall"]),
|
||||
("whatsapp", build_whatsapp_parser, "cmd_whatsapp", ["whatsapp"]),
|
||||
("slack", build_slack_parser, "cmd_slack", ["slack"]),
|
||||
("login", build_login_parser, "cmd_login", ["login"]),
|
||||
("logout", build_logout_parser, "cmd_logout", ["logout"]),
|
||||
("auth", build_auth_parser, "cmd_auth", ["auth"]),
|
||||
("status", build_status_parser, "cmd_status", ["status"]),
|
||||
("webhook", build_webhook_parser, "cmd_webhook", ["webhook"]),
|
||||
("hooks", build_hooks_parser, "cmd_hooks", ["hooks"]),
|
||||
("doctor", build_doctor_parser, "cmd_doctor", ["doctor"]),
|
||||
("security", build_security_parser, "cmd_security", ["security"]),
|
||||
("dump", build_dump_parser, "cmd_dump", ["dump"]),
|
||||
("debug", build_debug_parser, "cmd_debug", ["debug"]),
|
||||
("backup", build_backup_parser, "cmd_backup", ["backup"]),
|
||||
("import", build_import_cmd_parser, "cmd_import", ["import", "/tmp/x.zip"]),
|
||||
("config", build_config_parser, "cmd_config", ["config"]),
|
||||
("version", build_version_parser, "cmd_version", ["version"]),
|
||||
("update", build_update_parser, "cmd_update", ["update"]),
|
||||
("uninstall", build_uninstall_parser, "cmd_uninstall", ["uninstall"]),
|
||||
("gui", build_gui_parser, "cmd_gui", ["gui"]),
|
||||
("logs", build_logs_parser, "cmd_logs", ["logs"]),
|
||||
("prompt-size", build_prompt_size_parser, "cmd_prompt_size", ["prompt-size"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,builder,kw,argv", SINGLE_HANDLER_CASES, ids=[c[0] for c in SINGLE_HANDLER_CASES])
|
||||
def test_single_handler_builders(name, builder, kw, argv):
|
||||
parser = argparse.ArgumentParser(prog="hermes")
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
handler = _h(name)
|
||||
builder(sub, **{kw: handler})
|
||||
ns = parser.parse_args(argv)
|
||||
assert ns.func is handler
|
||||
|
||||
|
||||
def test_dashboard_builder_two_handlers():
|
||||
parser = argparse.ArgumentParser(prog="hermes")
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
dash, reg = _h("dashboard"), _h("dashboard_register")
|
||||
build_dashboard_parser(sub, cmd_dashboard=dash, cmd_dashboard_register=reg)
|
||||
# bare dashboard -> launch handler
|
||||
assert parser.parse_args(["dashboard"]).func is dash
|
||||
# dashboard register -> register handler
|
||||
assert parser.parse_args(["dashboard", "register"]).func is reg
|
||||
Loading…
Add table
Add a link
Reference in a new issue