Hermes-agent
This commit is contained in:
@@ -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
|
||||
@@ -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)."
|
||||
),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user