refactor(hermes_cli/subcommands): pack argparse builder calls onto <=100-col lines (AST-verified)
This commit is contained in:
@@ -24,6 +24,4 @@ def add_accept_hooks_flag(parser: argparse.ArgumentParser) -> None:
|
||||
default=argparse.SUPPRESS,
|
||||
help=(
|
||||
"Auto-approve unseen shell hooks without a TTY prompt "
|
||||
"(equivalent to HERMES_ACCEPT_HOOKS=1 / hooks_auto_accept: true)."
|
||||
),
|
||||
)
|
||||
"(equivalent to HERMES_ACCEPT_HOOKS=1 / hooks_auto_accept: true)."))
|
||||
|
||||
@@ -10,39 +10,29 @@ 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",
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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).",
|
||||
)
|
||||
"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).",
|
||||
)
|
||||
"~400 MB Chromium download confirmation).")
|
||||
acp_parser.set_defaults(func=cmd_acp)
|
||||
|
||||
@@ -20,13 +20,9 @@ def build_approvals_parser(subparsers, *, cmd_approvals: Callable) -> None:
|
||||
"Tools for the dangerous-command approval system. "
|
||||
"`hermes approvals suggest` mines past approval decisions from "
|
||||
"the session database and proposes command_allowlist entries so "
|
||||
"repeatedly-approved commands stop prompting."
|
||||
),
|
||||
)
|
||||
"repeatedly-approved commands stop prompting."))
|
||||
approvals_subparsers = approvals_parser.add_subparsers(
|
||||
dest="approvals_command",
|
||||
metavar="<subcommand>",
|
||||
)
|
||||
dest="approvals_command", metavar="<subcommand>")
|
||||
|
||||
suggest_parser = approvals_subparsers.add_parser(
|
||||
"suggest",
|
||||
@@ -36,44 +32,26 @@ def build_approvals_parser(subparsers, *, cmd_approvals: Callable) -> None:
|
||||
"that ran with user approval, rank the recurring patterns, and "
|
||||
"print a numbered allowlist proposal. Nothing is written unless "
|
||||
"--apply is given. Destructive classes (recursive delete, sudo, "
|
||||
"disk writes, credential edits, ...) are never proposed."
|
||||
),
|
||||
)
|
||||
"disk writes, credential edits, ...) are never proposed."))
|
||||
suggest_parser.add_argument(
|
||||
"--apply",
|
||||
dest="apply_indices",
|
||||
metavar="N[,M...]",
|
||||
help="Merge the numbered proposals (from a prior run) into "
|
||||
"command_allowlist in config.yaml",
|
||||
)
|
||||
"command_allowlist in config.yaml")
|
||||
suggest_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit machine-readable JSON instead of human-readable text",
|
||||
)
|
||||
"--json", action="store_true",
|
||||
help="Emit machine-readable JSON instead of human-readable text")
|
||||
suggest_parser.add_argument(
|
||||
"--days",
|
||||
type=int,
|
||||
default=90,
|
||||
help="How far back to scan session history (default: 90; 0 = all)",
|
||||
)
|
||||
"--days", type=int, default=90,
|
||||
help="How far back to scan session history (default: 90; 0 = all)")
|
||||
suggest_parser.add_argument(
|
||||
"--min-count",
|
||||
dest="min_count",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Minimum approval count for a pattern to be proposed (default: 2)",
|
||||
)
|
||||
"--min-count", dest="min_count", type=int, default=2,
|
||||
help="Minimum approval count for a pattern to be proposed (default: 2)")
|
||||
suggest_parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Maximum number of proposals to show (default: 20)",
|
||||
)
|
||||
"--limit", type=int, default=20, help="Maximum number of proposals to show (default: 20)")
|
||||
suggest_parser.add_argument(
|
||||
"--db",
|
||||
help="Path to an alternate session database (default: ~/.hermes/state.db)",
|
||||
)
|
||||
"--db", help="Path to an alternate session database (default: ~/.hermes/state.db)")
|
||||
suggest_parser.set_defaults(func=cmd_approvals)
|
||||
|
||||
test_parser = approvals_subparsers.add_parser(
|
||||
@@ -87,29 +65,23 @@ def build_approvals_parser(subparsers, *, cmd_approvals: Callable) -> None:
|
||||
"executing the command, prompting anyone, or persisting anything. "
|
||||
"Exit codes: 0 allow, 2 ask-approval, 3 deny (hardline or user "
|
||||
"deny rule). Tip: use `--` before the command so its own flags "
|
||||
"aren't parsed: hermes approvals test -- rm -rf /tmp/x"
|
||||
),
|
||||
)
|
||||
"aren't parsed: hermes approvals test -- rm -rf /tmp/x"))
|
||||
test_parser.add_argument(
|
||||
"--env-type",
|
||||
dest="env_type",
|
||||
default="local",
|
||||
help="Terminal backend type to evaluate against (default: local; "
|
||||
"isolated container backends like docker skip the guards)",
|
||||
)
|
||||
"isolated container backends like docker skip the guards)")
|
||||
test_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit machine-readable JSON instead of human-readable text",
|
||||
)
|
||||
"--json", action="store_true",
|
||||
help="Emit machine-readable JSON instead of human-readable text")
|
||||
test_parser.add_argument(
|
||||
"command_words",
|
||||
nargs=argparse.REMAINDER,
|
||||
metavar="command",
|
||||
# NOTE: dest must NOT be "command" — main.py's startup path reads
|
||||
# args.command as the top-level subcommand name ("approvals").
|
||||
help="The command to evaluate (prefix with -- to protect its flags)",
|
||||
)
|
||||
help="The command to evaluate (prefix with -- to protect its flags)")
|
||||
test_parser.set_defaults(func=cmd_approvals)
|
||||
|
||||
approvals_parser.set_defaults(func=cmd_approvals)
|
||||
|
||||
@@ -7,88 +7,52 @@ 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_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)",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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("--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",
|
||||
)
|
||||
"--no-browser", action="store_true", help="Do not auto-open a browser for OAuth login")
|
||||
auth_add.add_argument("--timeout", type=float, help="OAuth/network timeout in seconds")
|
||||
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",
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"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_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"
|
||||
)
|
||||
"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 = 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"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
"spotify", help="Authenticate Hermes with Spotify via PKCE")
|
||||
auth_spotify.add_argument(
|
||||
"spotify_action",
|
||||
nargs="?",
|
||||
choices=["login", "status", "logout"],
|
||||
default="login",
|
||||
)
|
||||
"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)"
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"--timeout", type=float, help="Callback/token exchange timeout in seconds")
|
||||
auth_parser.set_defaults(func=cmd_auth)
|
||||
|
||||
@@ -12,20 +12,14 @@ def build_backup_parser(subparsers, *, cmd_backup: Callable) -> None:
|
||||
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.",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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)"
|
||||
)
|
||||
"-l", "--label", help="Label for the snapshot (only used with --quick)")
|
||||
backup_parser.set_defaults(func=cmd_backup)
|
||||
|
||||
@@ -15,27 +15,20 @@ def build_browser_parser(subparsers) -> None:
|
||||
"close-profile terminates the browser process tree holding your "
|
||||
"default profile so Hermes can copy it — DESTRUCTIVE (unsaved tabs "
|
||||
"in that browser are lost). The agent runs this only after you "
|
||||
"approve closing the browser."
|
||||
),
|
||||
)
|
||||
"approve closing the browser."))
|
||||
browser_subparsers = browser_parser.add_subparsers(dest="browser_action")
|
||||
browser_close = browser_subparsers.add_parser(
|
||||
"close-profile",
|
||||
help="Close the browser locking your real profile (asks nothing — "
|
||||
"run only with the user's explicit OK; loses unsaved tabs)",
|
||||
)
|
||||
"run only with the user's explicit OK; loses unsaved tabs)")
|
||||
browser_close.add_argument(
|
||||
"--browser",
|
||||
help="Override detected default browser (chrome/edge/brave/brave-origin/chromium)",
|
||||
)
|
||||
help="Override detected default browser (chrome/edge/brave/brave-origin/chromium)")
|
||||
|
||||
def _dispatch_browser(_args):
|
||||
from hermes_cli.browser_connect import (
|
||||
UNSUPPORTED_CHANNEL,
|
||||
close_browser_holding_profile,
|
||||
detect_default_chromium,
|
||||
real_profile_data_dir,
|
||||
)
|
||||
UNSUPPORTED_CHANNEL, close_browser_holding_profile, detect_default_chromium,
|
||||
real_profile_data_dir)
|
||||
|
||||
action = getattr(_args, "browser_action", None)
|
||||
if action != "close-profile":
|
||||
|
||||
@@ -11,9 +11,7 @@ def build_bundles_parser(subparsers) -> None:
|
||||
description=(
|
||||
"Skill bundles let you load several skills under one slash "
|
||||
"command. `/<bundle>` from the CLI or gateway loads every "
|
||||
"referenced skill at once."
|
||||
),
|
||||
)
|
||||
"referenced skill at once."))
|
||||
from hermes_cli.bundles import register_cli as _bundles_register, bundles_command
|
||||
_bundles_register(bundles_parser)
|
||||
bundles_parser.set_defaults(func=bundles_command)
|
||||
|
||||
@@ -11,7 +11,6 @@ def build_checkpoints_parser(subparsers) -> None:
|
||||
description="Manage the filesystem checkpoint store — the shadow git "
|
||||
"repo hermes uses to snapshot working directories before "
|
||||
"write_file/patch/terminal calls. Lets you see how much "
|
||||
"space checkpoints occupy, force a prune, or wipe the base.",
|
||||
)
|
||||
"space checkpoints occupy, force a prune, or wipe the base.")
|
||||
from hermes_cli.checkpoints import register_cli as _register_checkpoints_cli
|
||||
_register_checkpoints_cli(checkpoints_parser)
|
||||
|
||||
@@ -8,10 +8,8 @@ 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", 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
|
||||
@@ -19,70 +17,46 @@ def build_claw_parser(subparsers, *, cmd_claw: Callable) -> None:
|
||||
"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.",
|
||||
)
|
||||
"Always shows a preview before making changes.")
|
||||
claw_migrate.add_argument("--source", help="Path to OpenClaw directory (default: ~/.openclaw)")
|
||||
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",
|
||||
)
|
||||
"--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.",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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.",
|
||||
)
|
||||
"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').",
|
||||
)
|
||||
"before apply; restorable with 'hermes import').")
|
||||
claw_migrate.add_argument(
|
||||
"--workspace-target", help="Absolute path to copy workspace instructions into"
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"--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",
|
||||
"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(
|
||||
"--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"
|
||||
)
|
||||
"--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)
|
||||
|
||||
@@ -8,14 +8,8 @@ from typing import Callable
|
||||
def build_completion_parser(subparsers, *, cmd_completion: Callable, parser) -> None:
|
||||
"""Attach the ``completion`` subcommand to ``subparsers``."""
|
||||
completion_parser = subparsers.add_parser(
|
||||
"completion",
|
||||
help="Print shell completion script (bash, zsh, or fish)",
|
||||
)
|
||||
"completion", help="Print shell completion script (bash, zsh, or fish)")
|
||||
completion_parser.add_argument(
|
||||
"shell",
|
||||
nargs="?",
|
||||
default="bash",
|
||||
choices=["bash", "zsh", "fish"],
|
||||
help="Shell type (default: bash)",
|
||||
)
|
||||
"shell", nargs="?", default="bash", choices=["bash", "zsh", "fish"],
|
||||
help="Shell type (default: bash)")
|
||||
completion_parser.set_defaults(func=lambda args: cmd_completion(args, parser))
|
||||
|
||||
@@ -23,28 +23,19 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
"Use `hermes computer-use doctor` to run cua-driver's\n"
|
||||
"`health_report` MCP tool and surface its check matrix\n"
|
||||
"(TCC, bundle identity, version, platform support, ...)\n"
|
||||
"in human-readable form."
|
||||
),
|
||||
)
|
||||
"in human-readable form."))
|
||||
computer_use_sub = computer_use_parser.add_subparsers(dest="computer_use_action")
|
||||
|
||||
computer_use_install = computer_use_sub.add_parser(
|
||||
"install",
|
||||
help="Install or repair the cua-driver binary (macOS/Windows/Linux)",
|
||||
)
|
||||
"install", help="Install or repair the cua-driver binary (macOS/Windows/Linux)")
|
||||
computer_use_install.add_argument(
|
||||
"--upgrade",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Re-run the upstream installer even if cua-driver is already on "
|
||||
"PATH. The upstream install.sh always pulls the latest release, "
|
||||
"so this performs an in-place upgrade."
|
||||
),
|
||||
)
|
||||
computer_use_sub.add_parser(
|
||||
"status",
|
||||
help="Print whether cua-driver is installed and on PATH",
|
||||
)
|
||||
"so this performs an in-place upgrade."))
|
||||
computer_use_sub.add_parser("status", help="Print whether cua-driver is installed and on PATH")
|
||||
computer_use_doctor = computer_use_sub.add_parser(
|
||||
"doctor",
|
||||
help="Run cua-driver `health_report` and surface the check matrix",
|
||||
@@ -55,9 +46,7 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
"output. cua-driver owns the health model; this command stays\n"
|
||||
"thin so new checks added upstream surface here without code\n"
|
||||
"changes. Exits 0 when overall=ok, 1 when degraded/failed, 2\n"
|
||||
"when the binary is missing or unreachable."
|
||||
),
|
||||
)
|
||||
"when the binary is missing or unreachable."))
|
||||
computer_use_doctor.add_argument(
|
||||
"--include",
|
||||
action="append",
|
||||
@@ -66,21 +55,13 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
help=(
|
||||
"Run only the listed checks. Repeat for multiple "
|
||||
"(e.g. --include tcc_accessibility --include bundle_identity). "
|
||||
"Unknown names are reported by cua-driver."
|
||||
),
|
||||
)
|
||||
"Unknown names are reported by cua-driver."))
|
||||
computer_use_doctor.add_argument(
|
||||
"--skip",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="CHECK",
|
||||
help="Skip the listed checks. Repeat for multiple. Wins over --include.",
|
||||
)
|
||||
"--skip", action="append", default=[], metavar="CHECK",
|
||||
help="Skip the listed checks. Repeat for multiple. Wins over --include.")
|
||||
computer_use_doctor.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit the raw structured payload as JSON (same shape as `tools/call`).",
|
||||
)
|
||||
"--json", action="store_true",
|
||||
help="Emit the raw structured payload as JSON (same shape as `tools/call`).")
|
||||
computer_use_perms = computer_use_sub.add_parser(
|
||||
"permissions",
|
||||
help="Check or grant macOS Accessibility + Screen Recording (macOS)",
|
||||
@@ -89,32 +70,18 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
"attach to cua-driver's own identity (com.trycua.driver) — not the\n"
|
||||
"terminal or the Hermes app. `status` reports the driver's grant\n"
|
||||
"state; `grant` launches CuaDriver via LaunchServices so the macOS\n"
|
||||
"permission dialog is attributed to the process that does the work."
|
||||
),
|
||||
)
|
||||
computer_use_perms_sub = computer_use_perms.add_subparsers(
|
||||
dest="computer_use_perms_action"
|
||||
)
|
||||
"permission dialog is attributed to the process that does the work."))
|
||||
computer_use_perms_sub = computer_use_perms.add_subparsers(dest="computer_use_perms_action")
|
||||
computer_use_perms_status = computer_use_perms_sub.add_parser(
|
||||
"status",
|
||||
help="Report Accessibility + Screen Recording grant state (read-only)",
|
||||
)
|
||||
"status", help="Report Accessibility + Screen Recording grant state (read-only)")
|
||||
computer_use_perms_status.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit the normalized permission payload as JSON.",
|
||||
)
|
||||
"--json", action="store_true", help="Emit the normalized permission payload as JSON.")
|
||||
computer_use_perms_sub.add_parser(
|
||||
"grant",
|
||||
help="Request the grants (opens the dialog attributed to CuaDriver)",
|
||||
)
|
||||
"grant", help="Request the grants (opens the dialog attributed to CuaDriver)")
|
||||
def cmd_computer_use(args):
|
||||
action = getattr(args, "computer_use_action", None)
|
||||
if action == "install":
|
||||
from hermes_cli.tools_config import (
|
||||
_cua_driver_contract_status,
|
||||
install_cua_driver,
|
||||
)
|
||||
from hermes_cli.tools_config import (_cua_driver_contract_status, install_cua_driver)
|
||||
if not install_cua_driver(upgrade=bool(getattr(args, "upgrade", False))):
|
||||
return 1
|
||||
return 0 if _cua_driver_contract_status().get("ready") else 1
|
||||
@@ -123,9 +90,7 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
import subprocess
|
||||
from hermes_cli.tools_config import _cua_driver_contract_status
|
||||
from tools.computer_use.cua_backend import (
|
||||
cua_driver_update_check,
|
||||
resolve_cua_driver_cmd,
|
||||
)
|
||||
cua_driver_update_check, resolve_cua_driver_cmd)
|
||||
# Must match the runtime resolver: Desktop/TUI processes can omit
|
||||
# ~/.local/bin even though the official installer put the driver there.
|
||||
path = resolve_cua_driver_cmd()
|
||||
@@ -156,13 +121,11 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
if not contract.get("ready"):
|
||||
print(
|
||||
" ⚠ Repair required: "
|
||||
+ (contract.get("reason") or "runtime contract is incomplete")
|
||||
)
|
||||
+ (contract.get("reason") or "runtime contract is incomplete"))
|
||||
if override:
|
||||
print(
|
||||
" Update the binary selected by HERMES_CUA_DRIVER_CMD, or unset "
|
||||
"the override and run: hermes computer-use install --upgrade"
|
||||
)
|
||||
"the override and run: hermes computer-use install --upgrade")
|
||||
else:
|
||||
print(" Run: hermes computer-use install")
|
||||
return 1
|
||||
@@ -188,8 +151,7 @@ def build_computer_use_parser(subparsers) -> None:
|
||||
code = run_doctor(
|
||||
include=list(getattr(args, "include", []) or []),
|
||||
skip=list(getattr(args, "skip", []) or []),
|
||||
json_output=bool(getattr(args, "json", False)),
|
||||
)
|
||||
json_output=bool(getattr(args, "json", False)))
|
||||
sys.exit(code)
|
||||
if action == "permissions":
|
||||
perms_action = getattr(args, "computer_use_perms_action", None)
|
||||
|
||||
@@ -8,10 +8,8 @@ from typing import Callable
|
||||
def build_config_parser(subparsers, *, cmd_config: Callable) -> None:
|
||||
"""Attach the ``config`` subcommand to ``subparsers``."""
|
||||
config_parser = subparsers.add_parser(
|
||||
"config",
|
||||
help="View and edit configuration",
|
||||
description="Manage Hermes Agent configuration",
|
||||
)
|
||||
"config", help="View and edit configuration",
|
||||
description="Manage Hermes Agent configuration")
|
||||
config_subparsers = config_parser.add_subparsers(dest="config_command")
|
||||
|
||||
# config show (default)
|
||||
@@ -21,29 +19,23 @@ def build_config_parser(subparsers, *, cmd_config: Callable) -> None:
|
||||
config_subparsers.add_parser("edit", help="Open config file in editor")
|
||||
|
||||
# config get
|
||||
config_get = config_subparsers.add_parser(
|
||||
"get", help="Print a resolved configuration value"
|
||||
)
|
||||
config_get = config_subparsers.add_parser("get", help="Print a resolved configuration value")
|
||||
config_get.add_argument("key", nargs="?", help="Configuration key (e.g., model)")
|
||||
config_get.add_argument("--json", action="store_true", help="Print value as JSON")
|
||||
|
||||
# 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)"
|
||||
)
|
||||
"key", nargs="?", help="Configuration key (e.g., model, terminal.backend)")
|
||||
config_set.add_argument("value", nargs="?", help="Value to set")
|
||||
config_set.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Skip the unknown-key notice printed after writing a key the "
|
||||
"running version doesn't recognize (the value is saved either way).",
|
||||
)
|
||||
"running version doesn't recognize (the value is saved either way).")
|
||||
|
||||
# config unset
|
||||
config_unset = config_subparsers.add_parser(
|
||||
"unset", help="Remove a configuration value"
|
||||
)
|
||||
config_unset = config_subparsers.add_parser("unset", help="Remove a configuration value")
|
||||
config_unset.add_argument("key", nargs="?", help="Configuration key to remove")
|
||||
|
||||
# config path
|
||||
|
||||
@@ -12,7 +12,5 @@ def build_console_parser(subparsers, *, cmd_console: Callable) -> None:
|
||||
help="Open the safe Hermes command console",
|
||||
description=(
|
||||
"Open a curated Hermes command REPL. This is not a raw shell and "
|
||||
"does not expose the full Hermes CLI."
|
||||
),
|
||||
)
|
||||
"does not expose the full Hermes CLI."))
|
||||
console_parser.set_defaults(func=cmd_console)
|
||||
|
||||
@@ -10,8 +10,7 @@ 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", help="Cron job management", description="Manage scheduled tasks")
|
||||
cron_subparsers = cron_parser.add_subparsers(dest="cron_command")
|
||||
|
||||
# cron list
|
||||
@@ -20,23 +19,17 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
|
||||
# cron create/add
|
||||
cron_create = cron_subparsers.add_parser(
|
||||
"create", aliases=["add"], help="Create a scheduled job"
|
||||
)
|
||||
"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(
|
||||
"schedule", help="Schedule like '30m', 'every 2h', or '0 9 * * *'"
|
||||
)
|
||||
cron_create.add_argument(
|
||||
"prompt", nargs="?", help="Optional self-contained prompt or task instruction"
|
||||
)
|
||||
"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, "
|
||||
"platform:chat_id, or bot-chat[:profile] (inject output into a "
|
||||
"local profile's canonical Bot Chat as a message the bot responds to)"
|
||||
),
|
||||
)
|
||||
"local profile's canonical Bot Chat as a message the bot responds to)"))
|
||||
cron_create.add_argument(
|
||||
"--failure-deliver",
|
||||
dest="failure_deliver",
|
||||
@@ -44,16 +37,11 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
"Override target for FAILURE notices only (same grammar as "
|
||||
"--deliver). 'local' suppresses failure notices entirely; run "
|
||||
"state stays visible in `hermes cron list`. Omit = failures "
|
||||
"follow --deliver."
|
||||
),
|
||||
)
|
||||
"follow --deliver."))
|
||||
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.",
|
||||
)
|
||||
"--skill", dest="skills", action="append",
|
||||
help="Attach a skill. Repeat to add multiple skills.")
|
||||
cron_create.add_argument(
|
||||
"--script",
|
||||
help=(
|
||||
@@ -61,9 +49,7 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"else via Python."))
|
||||
cron_create.add_argument(
|
||||
"--no-agent",
|
||||
dest="no_agent",
|
||||
@@ -72,9 +58,7 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
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)."
|
||||
),
|
||||
)
|
||||
"pattern (memory alerts, disk alerts, CI pings)."))
|
||||
cron_create.add_argument(
|
||||
"--monitor-script",
|
||||
dest="monitor_script",
|
||||
@@ -85,18 +69,14 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
"entirely; changed output injects a MONITOR CHANGE DETECTED "
|
||||
"diff into the prompt. Script output must be stable (no "
|
||||
"timestamps). Mutually exclusive with --monitor-url; "
|
||||
"incompatible with --no-agent."
|
||||
),
|
||||
)
|
||||
"incompatible with --no-agent."))
|
||||
cron_create.add_argument(
|
||||
"--monitor-url",
|
||||
dest="monitor_url",
|
||||
help=(
|
||||
"Monitor mode: http(s) URL fetched with a bounded GET each tick "
|
||||
"instead of a script. Same hash-suppression semantics as "
|
||||
"--monitor-script."
|
||||
),
|
||||
)
|
||||
"--monitor-script."))
|
||||
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).",
|
||||
@@ -106,14 +86,10 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
help=(
|
||||
"Pin this job to a specific inference model (user-owned; the "
|
||||
"agent's cronjob tool cannot set this). Omit to follow "
|
||||
"cron.model / model.default from config.yaml."
|
||||
),
|
||||
)
|
||||
"cron.model / model.default from config.yaml."))
|
||||
cron_create.add_argument(
|
||||
"--provider",
|
||||
dest="model_provider",
|
||||
help="Inference provider paired with --model (e.g. 'openrouter', 'nous').",
|
||||
)
|
||||
"--provider", dest="model_provider",
|
||||
help="Inference provider paired with --model (e.g. 'openrouter', 'nous').")
|
||||
cron_create.add_argument(
|
||||
"--reasoning-effort",
|
||||
dest="reasoning_effort",
|
||||
@@ -121,9 +97,7 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
"Pin this job's reasoning (thinking) effort: none, minimal, low, "
|
||||
"medium, high, xhigh, max, or ultra. Overrides agent.reasoning_effort "
|
||||
"and agent.reasoning_overrides for this job; unsupported levels are "
|
||||
"clamped by the provider at request time. Omit to follow config."
|
||||
),
|
||||
)
|
||||
"clamped by the provider at request time. Omit to follow config."))
|
||||
cron_create.add_argument(
|
||||
"--continuity",
|
||||
dest="continuity",
|
||||
@@ -134,14 +108,10 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
"Each run wakes up with the job's own previous output injected "
|
||||
"into its prompt, so it can dedupe against what was already "
|
||||
"reported and continue where the last run left off (scouts, "
|
||||
"monitors, incremental digests). First run is unchanged."
|
||||
),
|
||||
)
|
||||
"monitors, incremental digests). First run is unchanged."))
|
||||
|
||||
# cron edit
|
||||
cron_edit = cron_subparsers.add_parser(
|
||||
"edit", help="Edit an existing scheduled job"
|
||||
)
|
||||
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")
|
||||
@@ -152,41 +122,25 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
dest="failure_deliver",
|
||||
help=(
|
||||
"Override target for failure notices (same grammar as --deliver; "
|
||||
"'local' suppresses; '' clears the override)"
|
||||
),
|
||||
)
|
||||
"'local' suppresses; '' clears the override)"))
|
||||
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.",
|
||||
)
|
||||
"--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.",
|
||||
)
|
||||
"--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.",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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."
|
||||
),
|
||||
)
|
||||
"injected into the agent's prompt each run."))
|
||||
cron_edit.add_argument(
|
||||
"--no-agent",
|
||||
dest="no_agent",
|
||||
@@ -195,16 +149,10 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
default=None,
|
||||
help=(
|
||||
"Enable no-agent mode on this job (requires --script or an "
|
||||
"existing script on the job)."
|
||||
),
|
||||
)
|
||||
"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).",
|
||||
)
|
||||
"--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(
|
||||
"--continuity",
|
||||
dest="continuity",
|
||||
@@ -213,34 +161,23 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
default=None,
|
||||
help=(
|
||||
"Turn on run-to-run continuity: each run sees the job's own "
|
||||
"previous output (dedupe, continue where it left off)."
|
||||
),
|
||||
)
|
||||
"previous output (dedupe, continue where it left off)."))
|
||||
cron_edit.add_argument(
|
||||
"--no-continuity",
|
||||
dest="continuity",
|
||||
action="store_const",
|
||||
const=False,
|
||||
help=(
|
||||
"Turn off run-to-run continuity (other context_from job refs "
|
||||
"are preserved)."
|
||||
),
|
||||
)
|
||||
help=("Turn off run-to-run continuity (other context_from job refs are preserved)."))
|
||||
cron_edit.add_argument(
|
||||
"--monitor-script",
|
||||
dest="monitor_script",
|
||||
help=(
|
||||
"Set/replace the monitor source script (see `hermes cron create "
|
||||
"--monitor-script`). Pass empty string to clear."
|
||||
),
|
||||
)
|
||||
"--monitor-script`). Pass empty string to clear."))
|
||||
cron_edit.add_argument(
|
||||
"--monitor-url",
|
||||
dest="monitor_url",
|
||||
help=(
|
||||
"Set/replace the monitor source URL. Pass empty string to clear."
|
||||
),
|
||||
)
|
||||
help=("Set/replace the monitor source URL. Pass empty string to clear."))
|
||||
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.",
|
||||
@@ -250,23 +187,17 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
help=(
|
||||
"Pin this job to a specific inference model (user-owned; the "
|
||||
"agent's cronjob tool cannot set this). Pass empty string to "
|
||||
"clear the pin and follow cron.model / model.default."
|
||||
),
|
||||
)
|
||||
"clear the pin and follow cron.model / model.default."))
|
||||
cron_edit.add_argument(
|
||||
"--provider",
|
||||
dest="model_provider",
|
||||
help="Inference provider paired with --model. Pass empty string to clear.",
|
||||
)
|
||||
"--provider", dest="model_provider",
|
||||
help="Inference provider paired with --model. Pass empty string to clear.")
|
||||
cron_edit.add_argument(
|
||||
"--reasoning-effort",
|
||||
dest="reasoning_effort",
|
||||
help=(
|
||||
"Pin this job's reasoning (thinking) effort: none, minimal, low, "
|
||||
"medium, high, xhigh, max, or ultra. Pass empty string to clear "
|
||||
"the pin and follow config resolution."
|
||||
),
|
||||
)
|
||||
"the pin and follow config resolution."))
|
||||
|
||||
# lifecycle actions
|
||||
cron_pause = cron_subparsers.add_parser("pause", help="Pause a scheduled job")
|
||||
@@ -277,60 +208,41 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
|
||||
cron_resume.add_argument("--at", dest="run_at", help="Re-arm at an ISO-8601 time")
|
||||
cron_resume.add_argument("--run-now", action="store_true", help="Re-arm to run now")
|
||||
|
||||
cron_run = cron_subparsers.add_parser(
|
||||
"run", help="Run a job on the next scheduler tick"
|
||||
)
|
||||
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"
|
||||
)
|
||||
"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_runs = cron_subparsers.add_parser(
|
||||
"runs", aliases=["history"], help="Show durable execution attempts"
|
||||
)
|
||||
"runs", aliases=["history"], help="Show durable execution attempts")
|
||||
cron_runs.add_argument("job_id", nargs="?", help="Optional job ID filter")
|
||||
cron_runs.add_argument("--limit", type=int, default=20, help="Rows to show (1-500)")
|
||||
|
||||
# cron incidents — durable failure incidents (list/ack)
|
||||
cron_incidents = cron_subparsers.add_parser(
|
||||
"incidents", help="List or acknowledge durable cron failure incidents"
|
||||
)
|
||||
"incidents", help="List or acknowledge durable cron failure incidents")
|
||||
cron_incidents.add_argument(
|
||||
"--state",
|
||||
choices=["detected", "alerted", "closed"],
|
||||
help="Filter incidents by lifecycle state",
|
||||
)
|
||||
"--state", choices=["detected", "alerted", "closed"],
|
||||
help="Filter incidents by lifecycle state")
|
||||
cron_incidents.add_argument(
|
||||
"incident_action",
|
||||
nargs="?",
|
||||
default="list",
|
||||
choices=["list", "ack"],
|
||||
help="Action (default: list)",
|
||||
)
|
||||
cron_incidents.add_argument(
|
||||
"incident_id", nargs="?", help="Incident ID to acknowledge (ack)"
|
||||
)
|
||||
"incident_action", nargs="?", default="list", choices=["list", "ack"],
|
||||
help="Action (default: list)")
|
||||
cron_incidents.add_argument("incident_id", nargs="?", help="Incident ID to acknowledge (ack)")
|
||||
|
||||
# cron notepad — per-job durable KV scratchpad (injected into the job
|
||||
# prompt each run; the running agent writes it via this CLI).
|
||||
cron_notepad = cron_subparsers.add_parser(
|
||||
"notepad",
|
||||
help="Read/write a job's durable notepad (persistent KV across runs)",
|
||||
)
|
||||
"notepad", help="Read/write a job's durable notepad (persistent KV across runs)")
|
||||
cron_notepad.add_argument("job_id", help="Job ID the notepad belongs to")
|
||||
cron_notepad.add_argument(
|
||||
"notepad_action",
|
||||
nargs="?",
|
||||
default="list",
|
||||
choices=["get", "set", "delete", "list"],
|
||||
help="Action (default: list)",
|
||||
)
|
||||
"notepad_action", nargs="?", default="list", choices=["get", "set", "delete", "list"],
|
||||
help="Action (default: list)")
|
||||
cron_notepad.add_argument("key", nargs="?", help="Notepad key (get/set/delete)")
|
||||
cron_notepad.add_argument("value", nargs="?", help="Value to store (set)")
|
||||
|
||||
|
||||
@@ -15,9 +15,7 @@ def build_curator_parser(subparsers) -> None:
|
||||
"periodically reviews agent-created skills, prunes stale ones, "
|
||||
"consolidates overlaps, and archives obsolete skills. "
|
||||
"Bundled and hub-installed skills are never touched. "
|
||||
"Archives are recoverable; auto-deletion never happens."
|
||||
),
|
||||
)
|
||||
"Archives are recoverable; auto-deletion never happens."))
|
||||
try:
|
||||
from hermes_cli.curator import register_cli as _register_curator_cli
|
||||
|
||||
|
||||
@@ -22,11 +22,8 @@ def _add_server_runtime_args(parser) -> None:
|
||||
browser-opening behavior and help framing differ.
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=9119, help="Port (default 9119, 0 for auto-assign by OS)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host", default="127.0.0.1", help="Host (default 127.0.0.1)"
|
||||
)
|
||||
"--port", type=int, default=9119, help="Port (default 9119, 0 for auto-assign by OS)")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="Host (default 127.0.0.1)")
|
||||
parser.add_argument(
|
||||
"--insecure",
|
||||
action="store_true",
|
||||
@@ -34,18 +31,14 @@ def _add_server_runtime_args(parser) -> None:
|
||||
"DEPRECATED / NO-OP. Formerly bypassed auth on a non-loopback "
|
||||
"bind. As of the June 2026 hardening it no longer disables "
|
||||
"authentication — a public bind always requires an auth provider "
|
||||
"(password or OAuth). Bind 127.0.0.1 + tunnel to keep it local."
|
||||
),
|
||||
)
|
||||
"(password or OAuth). Bind 127.0.0.1 + tunnel to keep it local."))
|
||||
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"
|
||||
),
|
||||
)
|
||||
"where npm may not be available. Pre-build with: cd web && npm run build"))
|
||||
parser.add_argument(
|
||||
"--isolated",
|
||||
action="store_true",
|
||||
@@ -53,17 +46,10 @@ def _add_server_runtime_args(parser) -> None:
|
||||
"When launched from a named profile, run a dedicated server scoped "
|
||||
"to that profile instead of routing to the machine-level server. "
|
||||
"Default behavior is unified: profile launches attach to (or start) "
|
||||
"ONE machine-level server and preselect the profile."
|
||||
),
|
||||
)
|
||||
"ONE machine-level server and preselect the profile."))
|
||||
# Internal flag set by the unified-launch re-exec (cmd_dashboard) to
|
||||
# preselect the launching profile in the SPA switcher. Hidden from --help.
|
||||
parser.add_argument(
|
||||
"--open-profile",
|
||||
dest="open_profile",
|
||||
default="",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
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 server has no
|
||||
@@ -71,15 +57,9 @@ def _add_server_runtime_args(parser) -> None:
|
||||
# `hermes dashboard` / `hermes serve` cmdlines and SIGTERM them directly —
|
||||
# the same path `hermes update` uses to clean up stale servers.
|
||||
parser.add_argument(
|
||||
"--stop",
|
||||
action="store_true",
|
||||
help="Stop all running Hermes web server processes and exit",
|
||||
)
|
||||
"--stop", action="store_true", help="Stop all running Hermes web server processes and exit")
|
||||
parser.add_argument(
|
||||
"--status",
|
||||
action="store_true",
|
||||
help="List running Hermes web server processes and exit",
|
||||
)
|
||||
"--status", action="store_true", help="List running Hermes web server processes and exit")
|
||||
|
||||
|
||||
def _configure_serve_parser(parser, *, cmd_dashboard: Callable) -> None:
|
||||
@@ -94,32 +74,16 @@ def _configure_serve_parser(parser, *, cmd_dashboard: Callable) -> None:
|
||||
# using the legacy flag do not trip an argparse error.
|
||||
parser.add_argument("--no-open", action="store_true", help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--ssh-session-token-file",
|
||||
dest="ssh_session_token_file",
|
||||
metavar="PATH",
|
||||
default=None,
|
||||
help="Read a one-shot Desktop SSH session token from PATH",
|
||||
)
|
||||
"--ssh-session-token-file", dest="ssh_session_token_file", metavar="PATH", default=None,
|
||||
help="Read a one-shot Desktop SSH session token from PATH")
|
||||
parser.add_argument(
|
||||
"--ssh-owner-nonce",
|
||||
dest="ssh_owner_nonce",
|
||||
metavar="NONCE",
|
||||
default=None,
|
||||
help="Identify a Desktop-owned SSH backend process",
|
||||
)
|
||||
parser.set_defaults(
|
||||
func=cmd_dashboard,
|
||||
no_open=True,
|
||||
headless_backend=True,
|
||||
command="serve",
|
||||
)
|
||||
"--ssh-owner-nonce", dest="ssh_owner_nonce", metavar="NONCE", default=None,
|
||||
help="Identify a Desktop-owned SSH backend process")
|
||||
parser.set_defaults(func=cmd_dashboard, no_open=True, headless_backend=True, command="serve")
|
||||
|
||||
|
||||
def build_serve_parser(
|
||||
*,
|
||||
cmd_dashboard: Callable,
|
||||
add_help: bool = True,
|
||||
exit_on_error: bool = True,
|
||||
*, cmd_dashboard: Callable, add_help: bool = True, exit_on_error: bool = True,
|
||||
) -> argparse.ArgumentParser:
|
||||
"""Build the standalone parser used by the lean ``serve`` dispatch path."""
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -127,18 +91,15 @@ def build_serve_parser(
|
||||
description=(
|
||||
"Run the Hermes backend server - the JSON-RPC/WebSocket gateway the "
|
||||
"desktop app and remote clients connect to. Headless: it never opens "
|
||||
"a browser UI."
|
||||
),
|
||||
"a browser UI."),
|
||||
add_help=add_help,
|
||||
exit_on_error=exit_on_error,
|
||||
)
|
||||
exit_on_error=exit_on_error)
|
||||
_configure_serve_parser(parser, cmd_dashboard=cmd_dashboard)
|
||||
return parser
|
||||
|
||||
|
||||
def build_dashboard_parser(
|
||||
subparsers, *, cmd_dashboard: Callable, cmd_dashboard_register: Callable
|
||||
) -> None:
|
||||
subparsers, *, cmd_dashboard: Callable, cmd_dashboard_register: Callable) -> None:
|
||||
"""Attach the ``dashboard`` and ``serve`` subcommands.
|
||||
|
||||
Both share the same backend (``cmd_dashboard`` → ``start_server``).
|
||||
@@ -148,14 +109,12 @@ def build_dashboard_parser(
|
||||
``dashboard``.
|
||||
"""
|
||||
dashboard_parser = subparsers.add_parser(
|
||||
"dashboard",
|
||||
help="Start the web UI dashboard",
|
||||
"dashboard", help="Start the web UI dashboard",
|
||||
description="Launch the Hermes Agent web dashboard for managing config, API keys, and sessions",
|
||||
)
|
||||
_add_server_runtime_args(dashboard_parser)
|
||||
dashboard_parser.add_argument(
|
||||
"--no-open", action="store_true", help="Don't open browser automatically"
|
||||
)
|
||||
"--no-open", action="store_true", help="Don't open browser automatically")
|
||||
# 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
|
||||
@@ -166,11 +125,7 @@ def build_dashboard_parser(
|
||||
# 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.add_argument("--tui", action="store_true", help=argparse.SUPPRESS)
|
||||
dashboard_parser.set_defaults(func=cmd_dashboard)
|
||||
|
||||
# serve command — the headless backend server
|
||||
@@ -186,18 +141,14 @@ def build_dashboard_parser(
|
||||
description=(
|
||||
"Run the Hermes backend server — the JSON-RPC/WebSocket gateway the "
|
||||
"desktop app and remote clients connect to. Headless: it never opens "
|
||||
"a browser UI."
|
||||
),
|
||||
)
|
||||
"a browser UI."))
|
||||
_configure_serve_parser(serve_parser, cmd_dashboard=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_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)",
|
||||
@@ -205,23 +156,17 @@ def build_dashboard_parser(
|
||||
"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)."
|
||||
),
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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."
|
||||
),
|
||||
)
|
||||
"https://hermes.example.com/auth/callback. Omit for localhost-only use."))
|
||||
dashboard_register_parser.add_argument(
|
||||
"--portal-url",
|
||||
dest="portal_url",
|
||||
@@ -230,7 +175,5 @@ def build_dashboard_parser(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"testing against a staging/preview portal."))
|
||||
dashboard_register_parser.set_defaults(func=cmd_dashboard_register)
|
||||
|
||||
@@ -29,26 +29,14 @@ Examples:
|
||||
)
|
||||
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", 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)",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--local", action="store_true", help="Print the report locally instead of uploading")
|
||||
share_parser.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
@@ -56,9 +44,7 @@ Examples:
|
||||
help=(
|
||||
"Skip the confirmation prompt and upload immediately. Required "
|
||||
"in non-interactive contexts (scripts/CI); without it, and with "
|
||||
"no TTY on stdin, the command refuses rather than upload silently."
|
||||
),
|
||||
)
|
||||
"no TTY on stdin, the command refuses rather than upload silently."))
|
||||
share_parser.add_argument(
|
||||
"--no-redact",
|
||||
action="store_true",
|
||||
@@ -66,9 +52,7 @@ Examples:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"into the public paste service."))
|
||||
share_parser.add_argument(
|
||||
"--nous",
|
||||
action="store_true",
|
||||
@@ -77,17 +61,10 @@ Examples:
|
||||
"of a public paste service. The bundle is private — viewable only "
|
||||
"by Nous staff (and allowlisted Discord mods) via a Google-login-"
|
||||
"gated viewer — and auto-deletes after 14 days. Still force-redacts "
|
||||
"secrets unless --no-redact is also passed."
|
||||
),
|
||||
)
|
||||
"secrets unless --no-redact is also passed."))
|
||||
delete_parser = debug_sub.add_parser(
|
||||
"delete",
|
||||
help="Delete a paste uploaded by 'hermes debug share'",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"urls", nargs="*", default=[],
|
||||
help="One or more paste URLs to delete (e.g. https://paste.rs/abc123)")
|
||||
debug_parser.set_defaults(func=cmd_debug)
|
||||
|
||||
@@ -8,22 +8,17 @@ from typing import Callable
|
||||
def build_doctor_parser(subparsers, *, cmd_doctor: Callable) -> None:
|
||||
"""Attach the ``doctor`` subcommand to ``subparsers``."""
|
||||
doctor_parser = subparsers.add_parser(
|
||||
"doctor",
|
||||
help="Check configuration and dependencies",
|
||||
description="Diagnose issues with Hermes Agent setup",
|
||||
)
|
||||
"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"
|
||||
)
|
||||
"--fix", action="store_true", help="Attempt to fix issues automatically")
|
||||
doctor_parser.add_argument(
|
||||
"--live",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Opt-in: run one bounded, read-only real-call health probe per "
|
||||
"configured tool backend (Firecrawl/FAL/browser/MCP/TTS/STT) "
|
||||
"after the static checks. Makes real network calls."
|
||||
),
|
||||
)
|
||||
"after the static checks. Makes real network calls."))
|
||||
doctor_parser.add_argument(
|
||||
"--ack",
|
||||
metavar="ADVISORY_ID",
|
||||
@@ -31,7 +26,5 @@ def build_doctor_parser(subparsers, *, cmd_doctor: Callable) -> 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` first to see active advisories and their IDs."))
|
||||
doctor_parser.set_defaults(func=cmd_doctor)
|
||||
|
||||
@@ -11,11 +11,8 @@ def build_dump_parser(subparsers, *, cmd_dump: Callable) -> None:
|
||||
"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",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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)
|
||||
|
||||
@@ -15,9 +15,7 @@ def build_egress_parser(subparsers) -> None:
|
||||
"Manage iron-proxy, the optional TLS-intercepting egress firewall "
|
||||
"that swaps proxy tokens for real API credentials before outbound "
|
||||
"requests leave a sandbox. Disabled by default. See: "
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/egress/iron-proxy"
|
||||
),
|
||||
)
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/egress/iron-proxy"))
|
||||
|
||||
from hermes_cli import proxy_cli as _proxy_cli
|
||||
_proxy_cli.register_cli(egress_parser)
|
||||
|
||||
@@ -14,26 +14,14 @@ def build_fallback_parser(subparsers) -> None:
|
||||
"Manage the fallback provider chain. Fallback providers are tried "
|
||||
"in order when the primary model fails with rate-limit, overload, or "
|
||||
"connection errors. See: "
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/features/fallback-providers"
|
||||
),
|
||||
)
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/features/fallback-providers"))
|
||||
fallback_subparsers = fallback_parser.add_subparsers(dest="fallback_command")
|
||||
fallback_subparsers.add_parser(
|
||||
"list",
|
||||
aliases=["ls"],
|
||||
help="Show the current fallback chain (default when no subcommand)",
|
||||
)
|
||||
"list", aliases=["ls"], help="Show the current fallback chain (default when no subcommand)")
|
||||
fallback_subparsers.add_parser(
|
||||
"add",
|
||||
help="Pick a provider + model (same picker as `hermes model`) and append to the chain",
|
||||
)
|
||||
help="Pick a provider + model (same picker as `hermes model`) and append to the chain")
|
||||
fallback_subparsers.add_parser(
|
||||
"remove",
|
||||
aliases=["rm"],
|
||||
help="Pick an entry to delete from the chain",
|
||||
)
|
||||
fallback_subparsers.add_parser(
|
||||
"clear",
|
||||
help="Remove all fallback entries",
|
||||
)
|
||||
"remove", aliases=["rm"], help="Pick an entry to delete from the chain")
|
||||
fallback_subparsers.add_parser("clear", help="Remove all fallback entries")
|
||||
fallback_parser.set_defaults(func=cmd_fallback)
|
||||
|
||||
@@ -16,11 +16,7 @@ def _add_compat_platform_flag(parser: argparse.ArgumentParser) -> None:
|
||||
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,
|
||||
)
|
||||
parser.add_argument("--platform", dest="platform", help=argparse.SUPPRESS)
|
||||
|
||||
|
||||
def build_gateway_parser(
|
||||
@@ -28,31 +24,22 @@ def build_gateway_parser(
|
||||
) -> None:
|
||||
"""Attach the ``gateway`` and ``proxy`` subcommands to ``subparsers``."""
|
||||
gateway_parser = subparsers.add_parser(
|
||||
"gateway",
|
||||
help="Messaging gateway management",
|
||||
description="Manage the messaging gateway (Telegram, Discord, WhatsApp, Weixin, and more)",
|
||||
)
|
||||
"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)"
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"-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)",
|
||||
)
|
||||
"--replace", action="store_true",
|
||||
help="Replace any existing gateway instance (useful for systemd)")
|
||||
gateway_run.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
@@ -60,9 +47,7 @@ def build_gateway_parser(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"corrupt shared gateway state."))
|
||||
gateway_run.add_argument(
|
||||
"--no-supervise",
|
||||
action="store_true",
|
||||
@@ -73,9 +58,7 @@ def build_gateway_parser(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"gateway's exit code. No effect outside an s6 container."))
|
||||
gateway_run.add_argument(
|
||||
"--external-supervisor",
|
||||
action="store_true",
|
||||
@@ -83,55 +66,34 @@ def build_gateway_parser(
|
||||
"Declare that an external process manager owns this foreground "
|
||||
"gateway. In-chat restarts and updates exit back to that manager "
|
||||
"instead of spawning a detached replacement. Use this when a "
|
||||
"launchd/systemd wrapper strips its native environment markers."
|
||||
),
|
||||
)
|
||||
"launchd/systemd wrapper strips its native environment markers."))
|
||||
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"
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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 = 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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--all", action="store_true",
|
||||
help="Kill ALL gateway processes across all profiles before restarting")
|
||||
_add_compat_platform_flag(gateway_restart)
|
||||
|
||||
# gateway status
|
||||
@@ -139,74 +101,41 @@ def build_gateway_parser(
|
||||
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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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="Start the gateway service immediately after installing",
|
||||
)
|
||||
"--start-now", dest="start_now", action="store_true", default=None,
|
||||
help="Start the gateway service immediately after installing")
|
||||
gateway_install.add_argument(
|
||||
"--no-start-now",
|
||||
dest="start_now",
|
||||
action="store_false",
|
||||
help="Do not start the gateway service after installing",
|
||||
)
|
||||
"--no-start-now", dest="start_now", action="store_false",
|
||||
help="Do not start the gateway service after installing")
|
||||
gateway_install.add_argument(
|
||||
"--start-on-login",
|
||||
dest="start_on_login",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="Enable the service to start automatically on login/boot",
|
||||
)
|
||||
"--start-on-login", dest="start_on_login", action="store_true", default=None,
|
||||
help="Enable the service to start automatically on login/boot")
|
||||
gateway_install.add_argument(
|
||||
"--no-start-on-login",
|
||||
dest="start_on_login",
|
||||
action="store_false",
|
||||
help="Do not enable the service to start on login/boot",
|
||||
)
|
||||
"--no-start-on-login", dest="start_on_login", action="store_false",
|
||||
help="Do not enable the service to start on login/boot")
|
||||
gateway_install.add_argument(
|
||||
"--elevated-handoff",
|
||||
dest="elevated_handoff",
|
||||
action="store_true",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
"--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 = 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",
|
||||
)
|
||||
"--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")
|
||||
@@ -222,22 +151,13 @@ def build_gateway_parser(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--yes", dest="yes", action="store_true", help="Skip the confirmation prompt")
|
||||
|
||||
# gateway enroll — enroll a self-hosted gateway with a relay connector
|
||||
# (connector⇄gateway auth). Redeems a single-use enrollment token for the
|
||||
@@ -253,17 +173,13 @@ def build_gateway_parser(
|
||||
"authoritative tenant from it), mints this gateway's per-gateway secret "
|
||||
"and per-tenant delivery key, and writes GATEWAY_RELAY_ID / "
|
||||
"GATEWAY_RELAY_SECRET / GATEWAY_RELAY_DELIVERY_KEY into ~/.hermes/.env. "
|
||||
"Requires being logged in (hermes setup). Not available in managed installs."
|
||||
),
|
||||
)
|
||||
"Requires being logged in (hermes setup). Not available in managed installs."))
|
||||
gateway_enroll.add_argument(
|
||||
"--token",
|
||||
default=None,
|
||||
help=(
|
||||
"The single-use enrollment token from the connector (delivered with "
|
||||
"your gateway config). Also settable via GATEWAY_RELAY_ENROLL_TOKEN."
|
||||
),
|
||||
)
|
||||
"your gateway config). Also settable via GATEWAY_RELAY_ENROLL_TOKEN."))
|
||||
gateway_enroll.add_argument(
|
||||
"--connector-url",
|
||||
dest="connector_url",
|
||||
@@ -271,18 +187,14 @@ def build_gateway_parser(
|
||||
help=(
|
||||
"The connector base/relay URL, e.g. wss://connector.example.com/relay "
|
||||
"or https://connector.example.com. Also settable via GATEWAY_RELAY_URL "
|
||||
"/ gateway.relay_url in config.yaml."
|
||||
),
|
||||
)
|
||||
"/ gateway.relay_url in config.yaml."))
|
||||
gateway_enroll.add_argument(
|
||||
"--gateway-id",
|
||||
dest="gateway_id",
|
||||
default=None,
|
||||
help=(
|
||||
"A stable id for this gateway instance (kill-switch granularity). "
|
||||
"Defaults to gw-<hostname>."
|
||||
),
|
||||
)
|
||||
"Defaults to gw-<hostname>."))
|
||||
gateway_enroll.add_argument(
|
||||
"--wake-url",
|
||||
dest="wake_url",
|
||||
@@ -293,9 +205,7 @@ def build_gateway_parser(
|
||||
"while it's idle/suspended, so it reconnects and drains. Persisted as "
|
||||
"GATEWAY_RELAY_WAKE_URL in ~/.hermes/.env and forwarded at provision. "
|
||||
"Optional — without it the gateway still drains whenever it next "
|
||||
"reconnects on its own."
|
||||
),
|
||||
)
|
||||
"reconnects on its own."))
|
||||
gateway_enroll.set_defaults(func=cmd_gateway_enroll)
|
||||
|
||||
# proxy command — local OpenAI-compatible proxy that attaches the user's
|
||||
@@ -309,36 +219,19 @@ def build_gateway_parser(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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 = 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`.",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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_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)
|
||||
|
||||
@@ -14,52 +14,35 @@ def build_gui_parser(subparsers, *, cmd_gui: Callable) -> None:
|
||||
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."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
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)",
|
||||
)
|
||||
help="Initial project directory for Desktop chat sessions (sets HERMES_DESKTOP_CWD)")
|
||||
gui_parser.add_argument(
|
||||
"--skip-build",
|
||||
action="store_true",
|
||||
"--skip-build", action="store_true",
|
||||
help="Skip npm install/package and launch the existing unpacked app from apps/desktop/release",
|
||||
)
|
||||
gui_parser.add_argument(
|
||||
"--local",
|
||||
action="store_true",
|
||||
help="Show the local-models UI in the desktop app (models pane, quickstart, picker rows)",
|
||||
)
|
||||
"--local", action="store_true",
|
||||
help="Show the local-models UI in the desktop app (models pane, quickstart, picker rows)")
|
||||
gui_parser.add_argument(
|
||||
"--force-build",
|
||||
action="store_true",
|
||||
help="Force a full rebuild even if the content stamp matches",
|
||||
)
|
||||
"--force-build", action="store_true",
|
||||
help="Force a full rebuild even if the content stamp matches")
|
||||
gui_parser.add_argument(
|
||||
"--setup-tcc-identity",
|
||||
action="store_true",
|
||||
@@ -69,12 +52,9 @@ def build_gui_parser(subparsers, *, cmd_gui: Callable) -> None:
|
||||
"it, then re-sign the packaged app. Makes macOS TCC grants (Full "
|
||||
"Disk Access, Accessibility, Files and Folders, microphone) survive "
|
||||
"rebuilds with a certificate-anchored identity. Idempotent — safe "
|
||||
"to re-run after updates."
|
||||
),
|
||||
)
|
||||
"to re-run after updates."))
|
||||
gui_parser.add_argument(
|
||||
"--identity",
|
||||
default="Hermes Local Signing",
|
||||
"--identity", default="Hermes Local Signing",
|
||||
help="Certificate name to create/use for --setup-tcc-identity (default: Hermes Local Signing)",
|
||||
)
|
||||
gui_parser.set_defaults(func=cmd_gui)
|
||||
|
||||
@@ -13,60 +13,42 @@ def build_hooks_parser(subparsers, *, cmd_hooks: Callable) -> None:
|
||||
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."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"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)"
|
||||
),
|
||||
)
|
||||
"(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"
|
||||
),
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"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"
|
||||
),
|
||||
)
|
||||
"JSON validity, and synthetic run timing"))
|
||||
|
||||
hooks_parser.set_defaults(func=cmd_hooks)
|
||||
|
||||
@@ -20,30 +20,17 @@ def build_import_agent_parser(subparsers, *, cmd_import_agent: Callable) -> None
|
||||
"Maps CLAUDE.md/AGENTS.md instructions, permission allowlists, MCP "
|
||||
"servers, skills, and memories into their Hermes equivalents. "
|
||||
"Always shows a preview before making changes. API keys and "
|
||||
"credentials are never imported — run 'hermes setup' for those."
|
||||
),
|
||||
)
|
||||
"credentials are never imported — run 'hermes setup' for those."))
|
||||
parser.add_argument(
|
||||
"agent",
|
||||
nargs="?",
|
||||
choices=["claude-code", "codex"],
|
||||
help="Which agent to import from (default: auto-detect ~/.claude or ~/.codex)",
|
||||
)
|
||||
"agent", nargs="?", choices=["claude-code", "codex"],
|
||||
help="Which agent to import from (default: auto-detect ~/.claude or ~/.codex)")
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
help="Path to the agent's config directory (default: ~/.claude or ~/.codex)",
|
||||
)
|
||||
"--source", help="Path to the agent's config directory (default: ~/.claude or ~/.codex)")
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview only — stop after showing what would be imported",
|
||||
)
|
||||
"--dry-run", action="store_true",
|
||||
help="Preview only — stop after showing what would be imported")
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite existing Hermes items on name conflicts (default: skip)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation prompts"
|
||||
)
|
||||
"--overwrite", action="store_true",
|
||||
help="Overwrite existing Hermes items on name conflicts (default: skip)")
|
||||
parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompts")
|
||||
parser.set_defaults(func=cmd_import_agent)
|
||||
|
||||
@@ -12,13 +12,9 @@ def build_import_cmd_parser(subparsers, *, cmd_import: Callable) -> None:
|
||||
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",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"-f", action="store_true", help="Overwrite existing files without confirmation")
|
||||
import_parser.set_defaults(func=cmd_import)
|
||||
|
||||
@@ -8,14 +8,11 @@ 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",
|
||||
"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)"
|
||||
)
|
||||
"--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.)"
|
||||
)
|
||||
"--source", help="Filter by platform (cli, telegram, discord, etc.)")
|
||||
insights_parser.set_defaults(func=cmd_insights)
|
||||
|
||||
@@ -15,9 +15,7 @@ def build_journey_parser(subparsers) -> None:
|
||||
"A terminal rendition of the desktop Star Map / Memory Graph: a "
|
||||
"timeline bar chart of learned skills and memories over time "
|
||||
"(oldest at top, newest at bottom) plus a playable constellation "
|
||||
"scrubber. Mirrors the TUI `/journey` overlay and the desktop panel."
|
||||
),
|
||||
)
|
||||
"scrubber. Mirrors the TUI `/journey` overlay and the desktop panel."))
|
||||
try:
|
||||
from hermes_cli.journey import register_cli as _register_journey_cli
|
||||
|
||||
|
||||
@@ -29,46 +29,25 @@ def build_login_parser(subparsers, *, cmd_login: Callable) -> None:
|
||||
"login",
|
||||
description=(
|
||||
"Deprecated. Use `hermes auth` to manage credentials, "
|
||||
"`hermes model` to select a provider, or `hermes setup` for full setup."
|
||||
),
|
||||
)
|
||||
"`hermes model` to select a provider, or `hermes setup` for full setup."))
|
||||
# No ``choices=`` on purpose — the handler is a deprecation notice that
|
||||
# ignores the value, and a restrictive list would reject providers the user
|
||||
# legitimately wants (e.g. ``anthropic``) with an argparse error before the
|
||||
# friendly redirect message is ever printed.
|
||||
login_parser.add_argument(
|
||||
"--provider",
|
||||
default=None,
|
||||
help="(deprecated) Provider name; ignored — see `hermes model`",
|
||||
)
|
||||
"--provider", default=None, help="(deprecated) Provider name; ignored — see `hermes model`")
|
||||
login_parser.add_argument("--portal-url", help="Portal base URL (default: production portal)")
|
||||
login_parser.add_argument(
|
||||
"--portal-url", help="Portal base URL (default: production portal)"
|
||||
)
|
||||
"--inference-url", help="Inference API base URL (default: production inference API)")
|
||||
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)"
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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(
|
||||
"--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)",
|
||||
)
|
||||
"--insecure", action="store_true", help="Disable TLS verification (testing only)")
|
||||
login_parser.set_defaults(func=cmd_login)
|
||||
|
||||
@@ -8,14 +8,9 @@ from typing import Callable
|
||||
def build_logout_parser(subparsers, *, cmd_logout: Callable) -> None:
|
||||
"""Attach the ``logout`` subcommand to ``subparsers``."""
|
||||
logout_parser = subparsers.add_parser(
|
||||
"logout",
|
||||
help="Clear authentication for an inference provider",
|
||||
description="Remove stored credentials and reset provider config",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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)
|
||||
|
||||
@@ -30,42 +30,22 @@ Examples:
|
||||
""",
|
||||
)
|
||||
logs_parser.add_argument(
|
||||
"log_name",
|
||||
nargs="?",
|
||||
default="agent",
|
||||
"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)",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--component", metavar="NAME",
|
||||
help="Filter by component: gateway, agent, tools, cli, cron, gui")
|
||||
logs_parser.set_defaults(func=cmd_logs)
|
||||
|
||||
@@ -17,26 +17,17 @@ def build_mcp_parser(subparsers, *, cmd_mcp: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"'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)",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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 = 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
|
||||
@@ -45,28 +36,17 @@ def build_mcp_parser(subparsers, *, cmd_mcp: Callable) -> None:
|
||||
# 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(
|
||||
"--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",
|
||||
)
|
||||
"--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(
|
||||
"--connect-timeout",
|
||||
type=float,
|
||||
help="Timeout in seconds for initial connection and tool discovery",
|
||||
)
|
||||
"--connect-timeout", type=float,
|
||||
help="Timeout in seconds for initial connection and tool discovery")
|
||||
mcp_add_p.add_argument(
|
||||
"--env",
|
||||
nargs="*",
|
||||
default=[],
|
||||
help="Environment variables for stdio servers (KEY=VALUE)",
|
||||
)
|
||||
"--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")
|
||||
@@ -76,47 +56,28 @@ def build_mcp_parser(subparsers, *, cmd_mcp: Callable) -> None:
|
||||
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 = 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",
|
||||
)
|
||||
"login", help="Force re-authentication for an OAuth-based MCP server")
|
||||
mcp_login_p.add_argument("name", help="Server name to re-authenticate")
|
||||
|
||||
mcp_reauth_p = mcp_sub.add_parser(
|
||||
"reauth",
|
||||
help="Re-authenticate one OAuth MCP server, or all of them (--all)",
|
||||
)
|
||||
"reauth", help="Re-authenticate one OAuth MCP server, or all of them (--all)")
|
||||
mcp_reauth_p.add_argument(
|
||||
"name", nargs="?", help="Server name to re-authenticate (omit with --all)"
|
||||
)
|
||||
"name", nargs="?", help="Server name to re-authenticate (omit with --all)")
|
||||
mcp_reauth_p.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Re-authenticate every OAuth server in config, one at a time",
|
||||
)
|
||||
"--all", action="store_true",
|
||||
help="Re-authenticate every OAuth server in config, one at a time")
|
||||
|
||||
# ── 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",
|
||||
)
|
||||
"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>`)",
|
||||
)
|
||||
"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)
|
||||
|
||||
@@ -15,35 +15,19 @@ def build_memory_parser(subparsers, *, cmd_memory: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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", 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",
|
||||
)
|
||||
"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", 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(
|
||||
"--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'",
|
||||
)
|
||||
"--target", choices=["all", "memory", "user"], default="all",
|
||||
help="Which store to reset: 'all' (default), 'memory', or 'user'")
|
||||
memory_parser.set_defaults(func=cmd_memory)
|
||||
|
||||
@@ -12,9 +12,7 @@ def build_migrate_parser(subparsers) -> None:
|
||||
help="Migrate configuration for retired models or deprecated settings",
|
||||
description=(
|
||||
"Diagnose and (optionally) rewrite the active config.yaml to "
|
||||
"replace references to retired models or deprecated settings."
|
||||
),
|
||||
)
|
||||
"replace references to retired models or deprecated settings."))
|
||||
migrate_subparsers = migrate_parser.add_subparsers(dest="migrate_type")
|
||||
|
||||
migrate_xai = migrate_subparsers.add_parser(
|
||||
@@ -24,18 +22,12 @@ def build_migrate_parser(subparsers) -> None:
|
||||
"Scan config.yaml for references to xAI models retiring on "
|
||||
"May 15, 2026 and, with --apply, rewrite them in-place to the "
|
||||
"official replacements per the xAI migration guide. The original "
|
||||
"config.yaml is backed up before any rewrite."
|
||||
),
|
||||
)
|
||||
"config.yaml is backed up before any rewrite."))
|
||||
migrate_xai.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Rewrite config.yaml in-place (default: dry-run, no writes)",
|
||||
)
|
||||
"--apply", action="store_true",
|
||||
help="Rewrite config.yaml in-place (default: dry-run, no writes)")
|
||||
migrate_xai.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Skip the timestamped backup of config.yaml when applying",
|
||||
)
|
||||
"--no-backup", action="store_true",
|
||||
help="Skip the timestamped backup of config.yaml when applying")
|
||||
migrate_xai.set_defaults(func=cmd_migrate_xai)
|
||||
migrate_parser.set_defaults(func=cmd_migrate)
|
||||
|
||||
@@ -8,10 +8,8 @@ def build_moa_parser(subparsers) -> None:
|
||||
from hermes_cli.moa_cmd import cmd_moa
|
||||
|
||||
moa_parser = subparsers.add_parser(
|
||||
"moa",
|
||||
help="Configure Mixture of Agents provider/model slots",
|
||||
description="Configure the provider/model set used by /moa <prompt>.",
|
||||
)
|
||||
"moa", help="Configure Mixture of Agents provider/model slots",
|
||||
description="Configure the provider/model set used by /moa <prompt>.")
|
||||
moa_subparsers = moa_parser.add_subparsers(dest="moa_command")
|
||||
moa_subparsers.add_parser("list", aliases=["ls"], help="Show current MoA model slots")
|
||||
moa_configure = moa_subparsers.add_parser("configure", aliases=["config"], help="Interactively pick MoA models")
|
||||
|
||||
@@ -8,48 +8,29 @@ from typing import Callable
|
||||
def build_model_parser(subparsers, *, cmd_model: Callable) -> None:
|
||||
"""Attach the ``model`` subcommand to ``subparsers``."""
|
||||
model_parser = subparsers.add_parser(
|
||||
"model",
|
||||
help="Select default model and provider",
|
||||
description="Interactively select your inference provider and default model",
|
||||
)
|
||||
"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.",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
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)",
|
||||
)
|
||||
"--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(
|
||||
"--scope", default=None, help="OAuth scope to request for Nous login"
|
||||
)
|
||||
"--no-browser", action="store_true",
|
||||
help="Do not attempt to open the browser automatically during Nous login")
|
||||
model_parser.add_argument(
|
||||
"--no-browser",
|
||||
action="store_true",
|
||||
help="Do not attempt to open the browser automatically during Nous login",
|
||||
)
|
||||
"--timeout", type=float, default=15.0,
|
||||
help="HTTP request timeout in seconds for Nous login (default: 15)")
|
||||
model_parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=15.0,
|
||||
help="HTTP request timeout in seconds for Nous login (default: 15)",
|
||||
)
|
||||
"--ca-bundle", help="Path to CA bundle PEM file for Nous TLS verification")
|
||||
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)",
|
||||
)
|
||||
"--insecure", action="store_true",
|
||||
help="Disable TLS verification for Nous login (testing only)")
|
||||
model_parser.set_defaults(func=cmd_model)
|
||||
|
||||
@@ -23,14 +23,9 @@ def build_monitoring_parser(subparsers, *, cmd_monitoring: Callable) -> None:
|
||||
"diagnostics, exported over OTLP to an operator-configured "
|
||||
"endpoint. Content-free by construction — no prompts, messages, "
|
||||
"tool args/results, or usage analytics. Configure under "
|
||||
"monitoring.* in config.yaml."
|
||||
),
|
||||
)
|
||||
"monitoring.* in config.yaml."))
|
||||
sub = p.add_subparsers(dest="monitoring_action")
|
||||
|
||||
sub.add_parser(
|
||||
"status",
|
||||
help="Show monitoring settings, export state, and redaction posture",
|
||||
)
|
||||
sub.add_parser("status", help="Show monitoring settings, export state, and redaction posture")
|
||||
|
||||
p.set_defaults(func=cmd_monitoring)
|
||||
|
||||
@@ -8,25 +8,18 @@ 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", 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 request"
|
||||
)
|
||||
pairing_approve_parser = pairing_sub.add_parser("approve", help="Approve a pairing request")
|
||||
pairing_approve_parser.add_argument(
|
||||
"platform", help="Platform name (telegram, discord, slack, whatsapp)"
|
||||
)
|
||||
"platform", help="Platform name (telegram, discord, slack, whatsapp)")
|
||||
pairing_approve_parser.add_argument(
|
||||
"code",
|
||||
metavar="request-id|code",
|
||||
help="Request ID from 'pairing list', or the code the bot DM'd the user",
|
||||
)
|
||||
"code", metavar="request-id|code",
|
||||
help="Request ID from 'pairing list', or the code the bot DM'd the user")
|
||||
|
||||
pairing_revoke_parser = pairing_sub.add_parser("revoke", help="Revoke user access")
|
||||
pairing_revoke_parser.add_argument("platform", help="Platform name")
|
||||
|
||||
@@ -28,8 +28,7 @@ def cmd_pause(args: argparse.Namespace) -> int:
|
||||
print(f" sentinel: {path}")
|
||||
print(
|
||||
" Cron dispatch, kanban dispatch, and new gateway turns are on hold.\n"
|
||||
" In-flight work keeps running. Run `hermes resume` to lift the pause."
|
||||
)
|
||||
" In-flight work keeps running. Run `hermes resume` to lift the pause.")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -52,19 +51,12 @@ def build_pause_parser(subparsers) -> None:
|
||||
description=(
|
||||
"Engage the global emergency stop. Halts NEW work only — cron "
|
||||
"dispatch, kanban dispatch, and new gateway turns — until "
|
||||
"`hermes resume`. In-flight work is never killed."
|
||||
),
|
||||
)
|
||||
"`hermes resume`. In-flight work is never killed."))
|
||||
pause_parser.add_argument(
|
||||
"--reason",
|
||||
default=None,
|
||||
help="Optional reason stored in the sentinel and shown to users",
|
||||
)
|
||||
"--reason", default=None, help="Optional reason stored in the sentinel and shown to users")
|
||||
pause_parser.set_defaults(func=cmd_pause)
|
||||
|
||||
resume_parser = subparsers.add_parser(
|
||||
"resume",
|
||||
help="Lift the emergency stop set by `hermes pause`",
|
||||
description="Remove the ESTOP sentinel; dispatch resumes on the next tick.",
|
||||
)
|
||||
"resume", help="Lift the emergency stop set by `hermes pause`",
|
||||
description="Remove the ESTOP sentinel; dispatch resumes on the next tick.")
|
||||
resume_parser.set_defaults(func=cmd_resume)
|
||||
|
||||
@@ -84,29 +84,17 @@ def _peer_secret(name: str) -> str:
|
||||
|
||||
|
||||
def _request(
|
||||
url: str,
|
||||
key: str,
|
||||
*,
|
||||
method: str = "GET",
|
||||
body: dict | None = None,
|
||||
timeout: int = LIST_TIMEOUT_S,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> dict:
|
||||
url: str, key: str, *, method: str = "GET", body: dict | None = None,
|
||||
timeout: int = LIST_TIMEOUT_S, headers: dict[str, str] | None = None) -> dict:
|
||||
from hermes_cli.urllib_security import open_credentialed_url
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
request_headers = {
|
||||
"Authorization": f"Bearer {key}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "hermes-peer-dm",
|
||||
}
|
||||
"User-Agent": "hermes-peer-dm"}
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
method=method,
|
||||
headers=request_headers,
|
||||
)
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=request_headers)
|
||||
# The peer URL is user-registered (``hermes peer add``); a redirect to a
|
||||
# different origin must not carry the Authorization: Bearer key with it —
|
||||
# a compromised/MITM'd peer could otherwise harvest it. open_credentialed_url
|
||||
@@ -155,11 +143,8 @@ def _ensure_bot_chat(base: str, key: str) -> str:
|
||||
return existing
|
||||
try:
|
||||
created = _request(
|
||||
f"{base}/api/sessions",
|
||||
key,
|
||||
method="POST",
|
||||
body={"title": BOT_CHAT_TITLE, "source": "bot_peer_dm"},
|
||||
)
|
||||
f"{base}/api/sessions", key, method="POST",
|
||||
body={"title": BOT_CHAT_TITLE, "source": "bot_peer_dm"})
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = _http_error_detail(exc)
|
||||
if exc.code == 400 and "title" in detail.lower():
|
||||
@@ -214,8 +199,7 @@ def _resolve_peer_target(target: str) -> tuple[str, str | None, dict, str]:
|
||||
if not key:
|
||||
raise PermissionError(
|
||||
f"No API key for peer '{peer_name}'. Set it: hermes peer add {peer_name} "
|
||||
f"--url <url> --key <key> (or add {_peer_key_env(peer_name)}=<key> to ~/.hermes/.env)"
|
||||
)
|
||||
f"--url <url> --key <key> (or add {_peer_key_env(peer_name)}=<key> to ~/.hermes/.env)")
|
||||
return peer_name, profile, peer, key
|
||||
|
||||
|
||||
@@ -266,8 +250,7 @@ def cmd_peer(args) -> int:
|
||||
print(
|
||||
f"Peer '{name}' saved ({url}). No key given — set the peer's API_SERVER_KEY with:\n"
|
||||
f" hermes peer add {name} --url {url} --key <key>\n"
|
||||
f" (or add {_peer_key_env(name)}=<key> to ~/.hermes/.env)"
|
||||
)
|
||||
f" (or add {_peer_key_env(name)}=<key> to ~/.hermes/.env)")
|
||||
return 0
|
||||
|
||||
if action in ("remove", "rm"):
|
||||
@@ -316,13 +299,11 @@ def cmd_peer(args) -> int:
|
||||
+ ("/stop" if action == "stop" else ""),
|
||||
key,
|
||||
method="POST" if action == "stop" else "GET",
|
||||
body={} if action == "stop" else None,
|
||||
)
|
||||
body={} if action == "stop" else None)
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(
|
||||
f"Peer '{peer_name}' rejected the request (HTTP {exc.code}): {_http_error_detail(exc)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
except (urllib.error.URLError, TimeoutError, OSError, RuntimeError) as exc:
|
||||
print(f"Could not reach peer '{peer_name}': {exc}", file=sys.stderr)
|
||||
@@ -346,17 +327,14 @@ def cmd_peer(args) -> int:
|
||||
|
||||
if action == "run":
|
||||
idempotency_key = (
|
||||
getattr(args, "idempotency_key", None) or f"peer-{uuid.uuid4().hex}"
|
||||
).strip()
|
||||
getattr(args, "idempotency_key", None) or f"peer-{uuid.uuid4().hex}").strip()
|
||||
if (
|
||||
not idempotency_key
|
||||
or len(idempotency_key) > 255
|
||||
or re.search(r"[\r\n\x00]", idempotency_key)
|
||||
):
|
||||
or re.search(r"[\r\n\x00]", idempotency_key)):
|
||||
print(
|
||||
"Idempotency key must be 1-255 characters without control newlines.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
durability = _peer_run_durability(base, key)
|
||||
@@ -364,22 +342,16 @@ def cmd_peer(args) -> int:
|
||||
print(
|
||||
"Warning: this peer does not advertise restart-durable "
|
||||
"run replay; keep the run ID and avoid blind retries "
|
||||
"after a gateway restart.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
"after a gateway restart.", file=sys.stderr)
|
||||
session_id = _ensure_bot_chat(base, key)
|
||||
result = _request(
|
||||
f"{base}/v1/runs",
|
||||
key,
|
||||
method="POST",
|
||||
f"{base}/v1/runs", key, method="POST",
|
||||
body={"input": message, "session_id": session_id},
|
||||
headers={"Idempotency-Key": idempotency_key},
|
||||
)
|
||||
headers={"Idempotency-Key": idempotency_key})
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(
|
||||
f"Peer '{peer_name}' rejected the request (HTTP {exc.code}): {_http_error_detail(exc)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
except (urllib.error.URLError, TimeoutError, OSError, RuntimeError) as exc:
|
||||
print(f"Could not reach peer '{peer_name}': {exc}", file=sys.stderr)
|
||||
@@ -396,8 +368,7 @@ def cmd_peer(args) -> int:
|
||||
"run_id": run_id,
|
||||
"status": result.get("status") or "started",
|
||||
"idempotency_key": idempotency_key,
|
||||
"replayed": bool(result.get("replayed", False)),
|
||||
}
|
||||
"replayed": bool(result.get("replayed", False))}
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(payload))
|
||||
else:
|
||||
@@ -410,12 +381,8 @@ def cmd_peer(args) -> int:
|
||||
try:
|
||||
session_id = _ensure_bot_chat(base, key)
|
||||
result = _request(
|
||||
f"{base}/api/sessions/{urllib.parse.quote(session_id, safe='')}/chat",
|
||||
key,
|
||||
method="POST",
|
||||
body={"message": message},
|
||||
timeout=DM_TIMEOUT_S,
|
||||
)
|
||||
f"{base}/api/sessions/{urllib.parse.quote(session_id, safe='')}/chat", key,
|
||||
method="POST", body={"message": message}, timeout=DM_TIMEOUT_S)
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(f"Peer '{peer_name}' rejected the request (HTTP {exc.code}): {_http_error_detail(exc)}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -451,8 +418,7 @@ def build_peer_parser(subparsers) -> None:
|
||||
"agent's canonical Bot Chat over the peer's API server and prints "
|
||||
"the reply — the cross-machine twin of 'hermes -p <bot> chat'. "
|
||||
"The peer must run the api_server platform; its API_SERVER_KEY is "
|
||||
"stored locally as a credential in ~/.hermes/.env."
|
||||
),
|
||||
"stored locally as a credential in ~/.hermes/.env."),
|
||||
epilog=(
|
||||
"Examples:\n"
|
||||
" hermes peer add spark --url http://spark.lan:8377 --key <API_SERVER_KEY>\n"
|
||||
@@ -464,10 +430,8 @@ def build_peer_parser(subparsers) -> None:
|
||||
" hermes peer stop spark run_abc123\n"
|
||||
" hermes peer remove spark\n"
|
||||
"\n"
|
||||
"Exit codes: 0 ok, 1 delivery/peer error, 2 usage error."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
"Exit codes: 0 ok, 1 delivery/peer error, 2 usage error."),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
peer_sub = parser.add_subparsers(dest="peer_action")
|
||||
|
||||
add_p = peer_sub.add_parser("add", aliases=["set"], help="Register (or update) a peer gateway")
|
||||
@@ -481,61 +445,33 @@ def build_peer_parser(subparsers) -> None:
|
||||
rm_p = peer_sub.add_parser("remove", aliases=["rm"], help="Remove a peer")
|
||||
rm_p.add_argument("name", help="Peer name")
|
||||
|
||||
dm_p = peer_sub.add_parser(
|
||||
"dm",
|
||||
help="Message an agent on a peer gateway and print its reply",
|
||||
)
|
||||
dm_p = peer_sub.add_parser("dm", help="Message an agent on a peer gateway and print its reply")
|
||||
dm_p.add_argument(
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)"
|
||||
)
|
||||
dm_p.add_argument(
|
||||
"message", nargs="?", default=None, help="Message text (or stdin)"
|
||||
)
|
||||
dm_p.add_argument(
|
||||
"--json", action="store_true", default=False, help="Emit a JSON result"
|
||||
)
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)")
|
||||
dm_p.add_argument("message", nargs="?", default=None, help="Message text (or stdin)")
|
||||
dm_p.add_argument("--json", action="store_true", default=False, help="Emit a JSON result")
|
||||
|
||||
run_p = peer_sub.add_parser(
|
||||
"run",
|
||||
help="Start a long peer turn asynchronously and return its run ID",
|
||||
)
|
||||
"run", help="Start a long peer turn asynchronously and return its run ID")
|
||||
run_p.add_argument(
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)"
|
||||
)
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)")
|
||||
run_p.add_argument("message", nargs="?", default=None, help="Message text (or stdin)")
|
||||
run_p.add_argument(
|
||||
"message", nargs="?", default=None, help="Message text (or stdin)"
|
||||
)
|
||||
run_p.add_argument(
|
||||
"--idempotency-key",
|
||||
default=None,
|
||||
help="Stable retry key (generated when omitted)",
|
||||
)
|
||||
run_p.add_argument(
|
||||
"--json", action="store_true", default=False, help="Emit a JSON result"
|
||||
)
|
||||
"--idempotency-key", default=None, help="Stable retry key (generated when omitted)")
|
||||
run_p.add_argument("--json", action="store_true", default=False, help="Emit a JSON result")
|
||||
|
||||
status_p = peer_sub.add_parser(
|
||||
"status",
|
||||
help="Read the status and final output of an asynchronous peer run",
|
||||
)
|
||||
"status", help="Read the status and final output of an asynchronous peer run")
|
||||
status_p.add_argument(
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)"
|
||||
)
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)")
|
||||
status_p.add_argument("run_id", help="Run ID returned by 'hermes peer run'")
|
||||
status_p.add_argument(
|
||||
"--json", action="store_true", default=False, help="Emit a JSON result"
|
||||
)
|
||||
status_p.add_argument("--json", action="store_true", default=False, help="Emit a JSON result")
|
||||
|
||||
stop_p = peer_sub.add_parser(
|
||||
"stop",
|
||||
help="Stop one asynchronous peer run without affecting another turn",
|
||||
)
|
||||
"stop", help="Stop one asynchronous peer run without affecting another turn")
|
||||
stop_p.add_argument(
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)"
|
||||
)
|
||||
"target", help="<peer> or <peer>/<agent> (named profile on a multiplexed peer)")
|
||||
stop_p.add_argument("run_id", help="Run ID returned by 'hermes peer run'")
|
||||
stop_p.add_argument(
|
||||
"--json", action="store_true", default=False, help="Emit a JSON result"
|
||||
)
|
||||
stop_p.add_argument("--json", action="store_true", default=False, help="Emit a JSON result")
|
||||
|
||||
parser.set_defaults(func=cmd_peer)
|
||||
|
||||
@@ -14,9 +14,7 @@ def build_pets_parser(subparsers) -> None:
|
||||
"Petdex (https://github.com/crafter-station/petdex) is a public "
|
||||
"gallery of animated sprite pets for coding agents. Install one "
|
||||
"and Hermes shows it reacting to agent activity across the CLI, "
|
||||
"TUI, and desktop app."
|
||||
),
|
||||
)
|
||||
"TUI, and desktop app."))
|
||||
try:
|
||||
from hermes_cli.pets import register_cli as _register_pets_cli
|
||||
|
||||
|
||||
@@ -12,130 +12,81 @@ def build_plugins_parser(subparsers, *, cmd_plugins: Callable) -> None:
|
||||
help="Manage and validate plugins",
|
||||
description=(
|
||||
"Install, update, remove, list, or validate native Hermes plugins "
|
||||
"and portable Agent Plugins v1 packages. Portable packages install disabled."
|
||||
),
|
||||
)
|
||||
"and portable Agent Plugins v1 packages. Portable packages install disabled."))
|
||||
plugins_subparsers = plugins_parser.add_subparsers(dest="plugins_action")
|
||||
|
||||
plugins_install = plugins_subparsers.add_parser(
|
||||
"install", help="Install a plugin from a Git URL, owner/repo, or index name"
|
||||
)
|
||||
"install", help="Install a plugin from a Git URL, owner/repo, or index name")
|
||||
plugins_install.add_argument(
|
||||
"identifier",
|
||||
help=(
|
||||
"Git URL, owner/repo shorthand (e.g. anpicasso/hermes-plugin-chrome-profiles), "
|
||||
"or a bare plugin name resolved through the community index "
|
||||
"(see `hermes plugins search`)"
|
||||
),
|
||||
)
|
||||
"(see `hermes plugins search`)"))
|
||||
plugins_install.add_argument(
|
||||
"--force",
|
||||
"-f",
|
||||
action="store_true",
|
||||
help="Remove existing plugin and reinstall",
|
||||
)
|
||||
"-f", action="store_true", help="Remove existing plugin and reinstall")
|
||||
plugins_install.add_argument(
|
||||
"--ref",
|
||||
metavar="COMMIT_SHA",
|
||||
help="Install exactly one immutable 40-character Git commit SHA",
|
||||
)
|
||||
"--ref", metavar="COMMIT_SHA",
|
||||
help="Install exactly one immutable 40-character Git commit SHA")
|
||||
_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)",
|
||||
)
|
||||
"--enable", action="store_true",
|
||||
help="Auto-enable the plugin after install (skip confirmation prompt)")
|
||||
_install_enable_group.add_argument(
|
||||
"--no-enable",
|
||||
action="store_true",
|
||||
"--no-enable", action="store_true",
|
||||
help="Install disabled (skip confirmation prompt); enable later with `hermes plugins enable <name>`",
|
||||
)
|
||||
|
||||
plugins_search = plugins_subparsers.add_parser(
|
||||
"search", help="Search the community plugin index"
|
||||
)
|
||||
"search", help="Search the community plugin index")
|
||||
plugins_search.add_argument(
|
||||
"term",
|
||||
nargs="?",
|
||||
default="",
|
||||
help="Search term matched fuzzily against name, description, and tags "
|
||||
"(omit to browse the full index)",
|
||||
)
|
||||
"(omit to browse the full index)")
|
||||
plugins_search.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
||||
plugins_search.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print machine-readable JSON",
|
||||
)
|
||||
"--capability", metavar="CAP",
|
||||
help="Filter by declared capability (e.g. tools, platform, commands)")
|
||||
plugins_search.add_argument(
|
||||
"--capability",
|
||||
metavar="CAP",
|
||||
help="Filter by declared capability (e.g. tools, platform, commands)",
|
||||
)
|
||||
plugins_search.add_argument(
|
||||
"--refresh",
|
||||
action="store_true",
|
||||
help="Bypass the local cache and re-fetch the index",
|
||||
)
|
||||
"--refresh", action="store_true", help="Bypass the local cache and re-fetch the index")
|
||||
|
||||
plugins_update = plugins_subparsers.add_parser(
|
||||
"update", help="Pull latest changes for an installed plugin"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
"list", aliases=["ls"], help="List installed plugins")
|
||||
plugins_list.add_argument("--enabled", action="store_true", help="Show only enabled plugins")
|
||||
plugins_list.add_argument(
|
||||
"--enabled",
|
||||
action="store_true",
|
||||
help="Show only enabled plugins",
|
||||
)
|
||||
"--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(
|
||||
"--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",
|
||||
)
|
||||
"--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 = plugins_subparsers.add_parser("enable", help="Enable a disabled plugin")
|
||||
plugins_enable.add_argument("name", help="Plugin name to enable")
|
||||
_enable_override_group = plugins_enable.add_mutually_exclusive_group()
|
||||
_enable_override_group.add_argument(
|
||||
"--allow-tool-override",
|
||||
action="store_true",
|
||||
help="Grant this plugin permission to replace built-in tools "
|
||||
"(e.g. shell_exec, write_file). Skips the confirmation prompt.",
|
||||
)
|
||||
"(e.g. shell_exec, write_file). Skips the confirmation prompt.")
|
||||
_enable_override_group.add_argument(
|
||||
"--no-allow-tool-override",
|
||||
action="store_true",
|
||||
help="Enable without granting built-in tool override (skip prompt).",
|
||||
)
|
||||
"--no-allow-tool-override", action="store_true",
|
||||
help="Enable without granting built-in tool override (skip prompt).")
|
||||
|
||||
plugins_disable = plugins_subparsers.add_parser(
|
||||
"disable", help="Disable a plugin without removing it"
|
||||
)
|
||||
"disable", help="Disable a plugin without removing it")
|
||||
plugins_disable.add_argument("name", help="Plugin name to disable")
|
||||
|
||||
plugins_capabilities = plugins_subparsers.add_parser(
|
||||
@@ -144,30 +95,18 @@ def build_plugins_parser(subparsers, *, cmd_plugins: Callable) -> None:
|
||||
description=(
|
||||
"Show each plugin's declared capabilities (from plugin.yaml) "
|
||||
"against what the user has granted. Capabilities are a consent "
|
||||
"and audit layer over host API surfaces — NOT a sandbox."
|
||||
),
|
||||
)
|
||||
"and audit layer over host API surfaces — NOT a sandbox."))
|
||||
plugins_capabilities.add_argument(
|
||||
"name",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Plugin id to inspect (omit to list all plugins with capabilities)",
|
||||
)
|
||||
"name", nargs="?", default=None,
|
||||
help="Plugin id to inspect (omit to list all plugins with capabilities)")
|
||||
|
||||
plugins_doctor = plugins_subparsers.add_parser(
|
||||
"doctor", help="Validate a plugin with the real runtime contracts"
|
||||
)
|
||||
"doctor", help="Validate a plugin with the real runtime contracts")
|
||||
plugins_doctor.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Plugin path or installed plugin id (default: current directory)",
|
||||
)
|
||||
"target", nargs="?", default=".",
|
||||
help="Plugin path or installed plugin id (default: current directory)")
|
||||
plugins_doctor.add_argument(
|
||||
"--ci",
|
||||
action="store_true",
|
||||
help="Exit non-zero when validation reports an error",
|
||||
)
|
||||
"--ci", action="store_true", help="Exit non-zero when validation reports an error")
|
||||
|
||||
plugins_pack = plugins_subparsers.add_parser(
|
||||
"pack",
|
||||
@@ -176,51 +115,30 @@ def build_plugins_parser(subparsers, *, cmd_plugins: Callable) -> None:
|
||||
"Install, export, or inspect plugin packs — a single YAML file "
|
||||
"pinning a set of plugins to exact commit SHAs, with optional "
|
||||
"non-secret config seeds. Installing a pack fans out to ordinary "
|
||||
"pinned installs; capability consent stays per-plugin."
|
||||
),
|
||||
)
|
||||
"pinned installs; capability consent stays per-plugin."))
|
||||
pack_subparsers = plugins_pack.add_subparsers(dest="pack_action")
|
||||
|
||||
pack_install = pack_subparsers.add_parser(
|
||||
"install", help="Review and install a pack from a file path or https URL"
|
||||
)
|
||||
pack_install.add_argument(
|
||||
"source", help="Path to a hermes-pack.yaml file, or an https:// URL"
|
||||
)
|
||||
"install", help="Review and install a pack from a file path or https URL")
|
||||
pack_install.add_argument("source", help="Path to a hermes-pack.yaml file, or an https:// URL")
|
||||
pack_install.add_argument(
|
||||
"--force",
|
||||
"-f",
|
||||
action="store_true",
|
||||
help="Reinstall plugins that already exist",
|
||||
)
|
||||
"-f", action="store_true", help="Reinstall plugins that already exist")
|
||||
|
||||
pack_export = pack_subparsers.add_parser(
|
||||
"export",
|
||||
help="Emit a pack YAML for the current install on stdout",
|
||||
)
|
||||
"export", help="Emit a pack YAML for the current install on stdout")
|
||||
pack_export.add_argument(
|
||||
"--enabled-only",
|
||||
action="store_true",
|
||||
help="Only include plugins currently in plugins.enabled",
|
||||
)
|
||||
"--enabled-only", action="store_true",
|
||||
help="Only include plugins currently in plugins.enabled")
|
||||
pack_export.add_argument(
|
||||
"--name",
|
||||
default="my-hermes-pack",
|
||||
help="Pack name to embed in the exported YAML",
|
||||
)
|
||||
"--name", default="my-hermes-pack", help="Pack name to embed in the exported YAML")
|
||||
|
||||
pack_show = pack_subparsers.add_parser(
|
||||
"show", help="Dry-run: parse and display a pack without installing"
|
||||
)
|
||||
pack_show.add_argument(
|
||||
"source", help="Path to a hermes-pack.yaml file, or an https:// URL"
|
||||
)
|
||||
"show", help="Dry-run: parse and display a pack without installing")
|
||||
pack_show.add_argument("source", help="Path to a hermes-pack.yaml file, or an https:// URL")
|
||||
|
||||
plugins_show = plugins_subparsers.add_parser(
|
||||
"show",
|
||||
aliases=["info"],
|
||||
help="Show details for a single plugin (including emits/listens)",
|
||||
)
|
||||
"show", aliases=["info"], help="Show details for a single plugin (including emits/listens)")
|
||||
plugins_show.add_argument("name", help="Plugin name or key to show")
|
||||
|
||||
plugins_parser.set_defaults(func=cmd_plugins)
|
||||
|
||||
@@ -8,44 +8,28 @@ from typing import Callable
|
||||
def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None:
|
||||
"""Attach the ``profile`` subcommand to ``subparsers``."""
|
||||
profile_parser = subparsers.add_parser(
|
||||
"profile",
|
||||
help="Manage profiles — multiple isolated Hermes instances",
|
||||
)
|
||||
"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 = 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 = 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(
|
||||
"profile_name", help="Profile name (lowercase, alphanumeric)"
|
||||
)
|
||||
"--clone", action="store_true",
|
||||
help="Copy config.yaml, .env, SOUL.md, and skills from active profile")
|
||||
profile_create.add_argument(
|
||||
"--clone",
|
||||
action="store_true",
|
||||
help="Copy config.yaml, .env, SOUL.md, and skills from active profile",
|
||||
)
|
||||
"--clone-all", action="store_true",
|
||||
help="Full copy of active profile (all state, excluding per-profile history)")
|
||||
profile_create.add_argument(
|
||||
"--clone-all",
|
||||
action="store_true",
|
||||
help="Full copy of active profile (all state, excluding per-profile history)",
|
||||
)
|
||||
"--clone-from", metavar="SOURCE",
|
||||
help="Source profile to clone from; implies --clone unless --clone-all is set")
|
||||
profile_create.add_argument(
|
||||
"--clone-from",
|
||||
metavar="SOURCE",
|
||||
help="Source profile to clone from; implies --clone unless --clone-all is set",
|
||||
)
|
||||
"--no-alias", action="store_true", help="Skip wrapper script creation")
|
||||
profile_create.add_argument(
|
||||
"--no-alias", action="store_true", help="Skip wrapper script creation"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--no-skills",
|
||||
action="store_true",
|
||||
"--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(
|
||||
@@ -53,96 +37,63 @@ def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None:
|
||||
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`.",
|
||||
)
|
||||
"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_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)",
|
||||
)
|
||||
"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_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)",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"(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)",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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 = 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(
|
||||
"--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)",
|
||||
)
|
||||
"--name", dest="alias_name", metavar="NAME",
|
||||
help="Custom alias name (default: profile name)")
|
||||
|
||||
profile_rename = profile_subparsers.add_parser(
|
||||
"rename",
|
||||
help="Rename a profile ('default': sets a display name; id unchanged)",
|
||||
)
|
||||
"rename", help="Rename a profile ('default': sets a display name; id unchanged)")
|
||||
profile_rename.add_argument("old_name", help="Current profile name")
|
||||
profile_rename.add_argument(
|
||||
"new_name",
|
||||
help="New profile name (for 'default': a display name — the canonical id stays 'default')",
|
||||
)
|
||||
help="New profile name (for 'default': a display name — the canonical id stays 'default')")
|
||||
|
||||
profile_export = profile_subparsers.add_parser(
|
||||
"export", help="Export a profile to archive"
|
||||
)
|
||||
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: a managed profile-exports/<name>-<timestamp>.tar.gz "
|
||||
"under the default Hermes home)",
|
||||
)
|
||||
"under the default Hermes home)")
|
||||
|
||||
profile_import = profile_subparsers.add_parser(
|
||||
"import", help="Import a profile from archive"
|
||||
)
|
||||
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)",
|
||||
)
|
||||
"--name", dest="import_name", metavar="NAME",
|
||||
help="Profile name (default: inferred from archive)")
|
||||
|
||||
# ---------- Distribution subcommands (issue #20456) ----------
|
||||
profile_install = profile_subparsers.add_parser(
|
||||
@@ -151,29 +102,19 @@ def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None:
|
||||
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)",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
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",
|
||||
)
|
||||
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)",
|
||||
)
|
||||
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",
|
||||
)
|
||||
"-y", "--yes", action="store_true", help="Skip manifest preview confirmation")
|
||||
|
||||
profile_update = profile_subparsers.add_parser(
|
||||
"update",
|
||||
@@ -182,23 +123,15 @@ def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
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)",
|
||||
)
|
||||
"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)
|
||||
|
||||
@@ -13,17 +13,10 @@ def build_prompt_size_parser(subparsers, *, cmd_prompt_size: Callable) -> None:
|
||||
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)."
|
||||
),
|
||||
)
|
||||
"JSON. Runs offline (no API call)."))
|
||||
prompt_size_parser.add_argument(
|
||||
"--platform",
|
||||
default="cli",
|
||||
help="Platform to simulate (cli, telegram, discord, ...). Default: cli",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--json", action="store_true", help="Emit the breakdown as JSON")
|
||||
prompt_size_parser.set_defaults(func=cmd_prompt_size)
|
||||
|
||||
@@ -12,22 +12,14 @@ def build_secrets_parser(subparsers) -> None:
|
||||
"Pull API keys from an external secret manager at process startup "
|
||||
"instead of storing them in ~/.hermes/.env. Supports Bitwarden "
|
||||
"Secrets Manager and 1Password. See: "
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/secrets/"
|
||||
),
|
||||
)
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/secrets/"))
|
||||
secrets_subparsers = secrets_parser.add_subparsers(dest="secrets_command")
|
||||
|
||||
secrets_bw = secrets_subparsers.add_parser(
|
||||
"bitwarden",
|
||||
aliases=["bw"],
|
||||
help="Bitwarden Secrets Manager integration",
|
||||
)
|
||||
"bitwarden", aliases=["bw"], help="Bitwarden Secrets Manager integration")
|
||||
|
||||
secrets_op = secrets_subparsers.add_parser(
|
||||
"onepassword",
|
||||
aliases=["op", "1password"],
|
||||
help="1Password (op:// references) integration",
|
||||
)
|
||||
"onepassword", aliases=["op", "1password"], help="1Password (op:// references) integration")
|
||||
|
||||
# Lazy-import secrets_cli: the module imports agent.secret_sources.bitwarden
|
||||
# which loads cryptography._rust.pyd. On Windows this maps the native
|
||||
|
||||
@@ -14,44 +14,24 @@ def build_security_parser(subparsers, *, cmd_security: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"Does NOT scan globally-installed packages or editor/browser extensions."))
|
||||
security_subparsers = security_parser.add_subparsers(
|
||||
dest="security_command",
|
||||
metavar="<subcommand>",
|
||||
)
|
||||
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", 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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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)
|
||||
|
||||
@@ -9,230 +9,144 @@ from typing import Callable
|
||||
def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"""Attach the ``sessions`` subcommand to ``subparsers``."""
|
||||
sessions_parser = subparsers.add_parser(
|
||||
"sessions",
|
||||
help="Manage session history (list, rename, export, prune, delete)",
|
||||
description="View and manage the SQLite session store",
|
||||
)
|
||||
"sessions", help="Manage session history (list, rename, export, prune, delete)",
|
||||
description="View and manage the SQLite session store")
|
||||
sessions_subparsers = sessions_parser.add_subparsers(dest="sessions_action")
|
||||
|
||||
sessions_list = sessions_subparsers.add_parser("list", help="List recent sessions")
|
||||
sessions_list.add_argument(
|
||||
"--source", help="Filter by source (cli, telegram, discord, etc.)"
|
||||
)
|
||||
sessions_list.add_argument(
|
||||
"--limit", type=int, default=20, help="Max sessions to show"
|
||||
)
|
||||
sessions_list.add_argument("--source", help="Filter by source (cli, telegram, discord, etc.)")
|
||||
sessions_list.add_argument("--limit", type=int, default=20, help="Max sessions to show")
|
||||
sessions_list.add_argument(
|
||||
"--workspace",
|
||||
metavar="NEEDLE",
|
||||
help="Only sessions in one workspace: a git repo root or project dir "
|
||||
"(matched by path substring or basename).",
|
||||
)
|
||||
"(matched by path substring or basename).")
|
||||
|
||||
def _add_session_filter_args(p, default_older_help):
|
||||
p.add_argument(
|
||||
"--older-than",
|
||||
metavar="AGE",
|
||||
help=default_older_help,
|
||||
)
|
||||
p.add_argument("--older-than", metavar="AGE", help=default_older_help)
|
||||
p.add_argument(
|
||||
"--newer-than",
|
||||
metavar="AGE",
|
||||
help="Only match sessions active within the last AGE "
|
||||
"(e.g. '5h', '2d') or after an ISO timestamp",
|
||||
)
|
||||
"(e.g. '5h', '2d') or after an ISO timestamp")
|
||||
p.add_argument(
|
||||
"--before",
|
||||
metavar="TIME",
|
||||
help="Only match sessions started before TIME "
|
||||
"(duration ago like '5h', or ISO timestamp like '2026-07-05 14:30')",
|
||||
)
|
||||
"(duration ago like '5h', or ISO timestamp like '2026-07-05 14:30')")
|
||||
p.add_argument(
|
||||
"--after",
|
||||
metavar="TIME",
|
||||
help="Only match sessions started at/after TIME "
|
||||
"(duration ago like '5h', or ISO timestamp)",
|
||||
)
|
||||
"(duration ago like '5h', or ISO timestamp)")
|
||||
p.add_argument("--source", help="Only match sessions from this source")
|
||||
p.add_argument("--title", help="Only match sessions whose title contains this substring")
|
||||
p.add_argument("--end-reason", help="Only match sessions with this end reason")
|
||||
p.add_argument(
|
||||
"--title", help="Only match sessions whose title contains this substring"
|
||||
)
|
||||
p.add_argument(
|
||||
"--end-reason", help="Only match sessions with this end reason"
|
||||
)
|
||||
p.add_argument(
|
||||
"--cwd", help="Only match sessions whose working directory is under this path"
|
||||
)
|
||||
p.add_argument(
|
||||
"--min-messages", type=int, help="Only match sessions with >= N messages"
|
||||
)
|
||||
p.add_argument(
|
||||
"--max-messages", type=int, help="Only match sessions with <= N messages"
|
||||
)
|
||||
"--cwd", help="Only match sessions whose working directory is under this path")
|
||||
p.add_argument("--min-messages", type=int, help="Only match sessions with >= N messages")
|
||||
p.add_argument("--max-messages", type=int, help="Only match sessions with <= N messages")
|
||||
p.add_argument(
|
||||
"--model",
|
||||
help="Only match sessions whose model name contains this substring "
|
||||
"(e.g. 'sonnet', 'gpt-5', 'hermes')",
|
||||
)
|
||||
"(e.g. 'sonnet', 'gpt-5', 'hermes')")
|
||||
p.add_argument(
|
||||
"--provider",
|
||||
help="Only match sessions billed through this provider "
|
||||
"(e.g. openrouter, anthropic, nous)",
|
||||
)
|
||||
"(e.g. openrouter, anthropic, nous)")
|
||||
p.add_argument("--user", help="Only match sessions from this user ID")
|
||||
p.add_argument("--chat-id", help="Only match sessions from this chat/channel ID")
|
||||
p.add_argument(
|
||||
"--user", help="Only match sessions from this user ID"
|
||||
)
|
||||
"--chat-type", help="Only match sessions with this chat type (e.g. dm, group)")
|
||||
p.add_argument(
|
||||
"--chat-id", help="Only match sessions from this chat/channel ID"
|
||||
)
|
||||
p.add_argument(
|
||||
"--chat-type",
|
||||
help="Only match sessions with this chat type (e.g. dm, group)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--branch",
|
||||
help="Only match sessions whose git branch contains this substring",
|
||||
)
|
||||
"--branch", help="Only match sessions whose git branch contains this substring")
|
||||
p.add_argument(
|
||||
"--min-tokens", type=int,
|
||||
help="Only match sessions with >= N total tokens (input+output)",
|
||||
)
|
||||
help="Only match sessions with >= N total tokens (input+output)")
|
||||
p.add_argument(
|
||||
"--max-tokens", type=int,
|
||||
help="Only match sessions with <= N total tokens (input+output)",
|
||||
)
|
||||
help="Only match sessions with <= N total tokens (input+output)")
|
||||
p.add_argument(
|
||||
"--min-cost", type=float,
|
||||
help="Only match sessions costing >= N USD (actual or estimated)",
|
||||
)
|
||||
help="Only match sessions costing >= N USD (actual or estimated)")
|
||||
p.add_argument(
|
||||
"--max-cost", type=float,
|
||||
help="Only match sessions costing <= N USD (actual or estimated)",
|
||||
)
|
||||
help="Only match sessions costing <= N USD (actual or estimated)")
|
||||
p.add_argument(
|
||||
"--min-tool-calls", type=int,
|
||||
help="Only match sessions with >= N tool calls",
|
||||
)
|
||||
"--min-tool-calls", type=int, help="Only match sessions with >= N tool calls")
|
||||
p.add_argument(
|
||||
"--max-tool-calls", type=int,
|
||||
help="Only match sessions with <= N tool calls",
|
||||
)
|
||||
"--max-tool-calls", type=int, help="Only match sessions with <= N tool calls")
|
||||
p.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="List matching sessions without changing anything",
|
||||
)
|
||||
p.add_argument(
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation"
|
||||
)
|
||||
"--dry-run", action="store_true",
|
||||
help="List matching sessions without changing anything")
|
||||
p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
|
||||
|
||||
sessions_export = sessions_subparsers.add_parser(
|
||||
"export", help="Export sessions to JSONL, Markdown, or QMD"
|
||||
)
|
||||
"export", help="Export sessions to JSONL, Markdown, or QMD")
|
||||
sessions_export.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
help=(
|
||||
"Output path. JSONL: file path (use - for stdout, required). "
|
||||
"md/qmd: output directory (default: <hermes home>/session-exports)"
|
||||
),
|
||||
)
|
||||
"md/qmd: output directory (default: <hermes home>/session-exports)"))
|
||||
sessions_export.add_argument(
|
||||
"--format",
|
||||
choices=["jsonl", "md", "qmd", "html", "trace"],
|
||||
default="jsonl",
|
||||
help=(
|
||||
"Export format (default: jsonl). 'trace' emits Claude Code JSONL "
|
||||
"for the Hugging Face Agent Trace Viewer"
|
||||
),
|
||||
)
|
||||
"for the Hugging Face Agent Trace Viewer"))
|
||||
sessions_export.add_argument(
|
||||
"--upload",
|
||||
action="store_true",
|
||||
help=(
|
||||
"trace only: upload to your Hugging Face traces dataset instead "
|
||||
"of writing a local file (needs HF_TOKEN)"
|
||||
),
|
||||
)
|
||||
"of writing a local file (needs HF_TOKEN)"))
|
||||
sessions_export.add_argument(
|
||||
"--public",
|
||||
action="store_true",
|
||||
help="trace --upload only: create/update a public dataset instead of private",
|
||||
)
|
||||
"--public", action="store_true",
|
||||
help="trace --upload only: create/update a public dataset instead of private")
|
||||
sessions_export.add_argument(
|
||||
"--no-redact",
|
||||
action="store_true",
|
||||
help=(
|
||||
"trace only: skip the forced secret redaction; "
|
||||
"only use after manual review"
|
||||
),
|
||||
)
|
||||
help=("trace only: skip the forced secret redaction; only use after manual review"))
|
||||
sessions_export.add_argument(
|
||||
"--only",
|
||||
choices=["user-prompts"],
|
||||
help=(
|
||||
"Export only a filtered view (user-prompts: one prompt record "
|
||||
"per line for jsonl, headed sections for md)"
|
||||
),
|
||||
)
|
||||
sessions_export.add_argument(
|
||||
"--session-id", help="Session ID or unique prefix to export"
|
||||
)
|
||||
"per line for jsonl, headed sections for md)"))
|
||||
sessions_export.add_argument("--session-id", help="Session ID or unique prefix to export")
|
||||
_add_session_filter_args(
|
||||
sessions_export,
|
||||
"Only export sessions older than AGE (duration like '5h'/'2d', "
|
||||
"bare number of days, or an ISO timestamp)",
|
||||
)
|
||||
sessions_export, "Only export sessions older than AGE (duration like '5h'/'2d', "
|
||||
"bare number of days, or an ISO timestamp)")
|
||||
sessions_export.add_argument(
|
||||
"--redact",
|
||||
action="store_true",
|
||||
help="Redact secrets (API keys, tokens, credentials) from exported content",
|
||||
)
|
||||
"--redact", action="store_true",
|
||||
help="Redact secrets (API keys, tokens, credentials) from exported content")
|
||||
sessions_export.add_argument(
|
||||
"--lineage",
|
||||
choices=["single", "logical"],
|
||||
default="single",
|
||||
help="md/qmd only: export one row or its compression lineage",
|
||||
)
|
||||
"--lineage", choices=["single", "logical"], default="single",
|
||||
help="md/qmd only: export one row or its compression lineage")
|
||||
sessions_export.add_argument(
|
||||
"--delete-after-verified",
|
||||
action="store_true",
|
||||
help="md/qmd only: after verified single-session export, delete that session (needs --yes)",
|
||||
)
|
||||
"--delete-after-verified", action="store_true",
|
||||
help="md/qmd only: after verified single-session export, delete that session (needs --yes)")
|
||||
sessions_export.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="md/qmd only: overwrite an existing export file",
|
||||
)
|
||||
"--force", action="store_true", help="md/qmd only: overwrite an existing export file")
|
||||
|
||||
sessions_delete = sessions_subparsers.add_parser(
|
||||
"delete", help="Delete a specific session"
|
||||
)
|
||||
sessions_delete = sessions_subparsers.add_parser("delete", help="Delete a specific session")
|
||||
sessions_delete.add_argument("session_id", help="Session ID to delete")
|
||||
sessions_delete.add_argument(
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation"
|
||||
)
|
||||
sessions_delete.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
|
||||
|
||||
sessions_prune = sessions_subparsers.add_parser(
|
||||
"prune",
|
||||
help="Delete old sessions (filterable by time window, source, title, ...)",
|
||||
)
|
||||
"prune", help="Delete old sessions (filterable by time window, source, title, ...)")
|
||||
_add_session_filter_args(
|
||||
sessions_prune,
|
||||
"Delete sessions older than AGE — days if bare number, or a duration "
|
||||
sessions_prune, "Delete sessions older than AGE — days if bare number, or a duration "
|
||||
"like '5h'/'2d'/'1w', or an ISO timestamp (bare prune with no filters "
|
||||
"defaults to 90 days; any filter matches all ages)",
|
||||
)
|
||||
"defaults to 90 days; any filter matches all ages)")
|
||||
sessions_prune.add_argument(
|
||||
"--include-archived",
|
||||
action="store_true",
|
||||
help="Also delete archived sessions (excluded by default)",
|
||||
)
|
||||
"--include-archived", action="store_true",
|
||||
help="Also delete archived sessions (excluded by default)")
|
||||
sessions_prune.add_argument(
|
||||
"--include-pinned",
|
||||
action="store_true",
|
||||
help="Also delete pinned sessions (excluded by default — pin is a keep flag)",
|
||||
)
|
||||
"--include-pinned", action="store_true",
|
||||
help="Also delete pinned sessions (excluded by default — pin is a keep flag)")
|
||||
sessions_prune.add_argument(
|
||||
"--never-active",
|
||||
action="store_true",
|
||||
@@ -240,24 +154,16 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"Instead of ended sessions, delete keyed gateway rows that were "
|
||||
"opened and never used (no messages, tokens, tool calls or title) "
|
||||
"and are older than AGE (default 30 days). Ordinary prune can "
|
||||
"never reach these — it only ever selects ended sessions"
|
||||
),
|
||||
)
|
||||
"never reach these — it only ever selects ended sessions"))
|
||||
|
||||
sessions_archive = sessions_subparsers.add_parser(
|
||||
"archive",
|
||||
help="Bulk-archive (soft-hide) sessions matching filters — no deletion",
|
||||
)
|
||||
"archive", help="Bulk-archive (soft-hide) sessions matching filters — no deletion")
|
||||
_add_session_filter_args(
|
||||
sessions_archive,
|
||||
"Only archive sessions older than AGE (duration like '5h'/'2d', "
|
||||
"bare number of days, or ISO timestamp)",
|
||||
)
|
||||
sessions_archive, "Only archive sessions older than AGE (duration like '5h'/'2d', "
|
||||
"bare number of days, or ISO timestamp)")
|
||||
|
||||
sessions_subparsers.add_parser(
|
||||
"optimize",
|
||||
help="Reclaim disk space: merge FTS5 segments + VACUUM (no data change)",
|
||||
)
|
||||
"optimize", help="Reclaim disk space: merge FTS5 segments + VACUUM (no data change)")
|
||||
|
||||
sessions_clean_markers = sessions_subparsers.add_parser(
|
||||
"clean-markers",
|
||||
@@ -270,21 +176,13 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"the affected rows once, in place, so long-lived sessions stop "
|
||||
"re-scanning/re-repairing the same rows on every resume. Only the "
|
||||
"content column is touched; tool_calls and every other column on "
|
||||
"the row are left untouched."
|
||||
),
|
||||
)
|
||||
"the row are left untouched."))
|
||||
sessions_clean_markers.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Report the affected row count without writing",
|
||||
)
|
||||
"--dry-run", action="store_true", default=False,
|
||||
help="Report the affected row count without writing")
|
||||
sessions_clean_markers.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Skip the timestamped state.db backup taken before writing (not recommended)",
|
||||
)
|
||||
"--no-backup", action="store_true", default=False,
|
||||
help="Skip the timestamped state.db backup taken before writing (not recommended)")
|
||||
|
||||
sessions_optimize_storage = sessions_subparsers.add_parser(
|
||||
"optimize-storage",
|
||||
@@ -297,21 +195,14 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"foreground with a progress bar, throttles so a running gateway "
|
||||
"stays responsive, and VACUUMs at the end. Safe to interrupt and "
|
||||
"re-run — it resumes where it left off. No conversation data is "
|
||||
"changed; only the search index is rebuilt."
|
||||
),
|
||||
)
|
||||
"changed; only the search index is rebuilt."))
|
||||
sessions_optimize_storage.add_argument(
|
||||
"--no-vacuum",
|
||||
action="store_true",
|
||||
default=False,
|
||||
"--no-vacuum", action="store_true", default=False,
|
||||
help="Skip the final VACUUM (index is rebuilt but freed pages aren't returned to the OS until a later VACUUM)",
|
||||
)
|
||||
sessions_optimize_storage.add_argument(
|
||||
"--yes", "-y",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Skip the disk-space confirmation prompt",
|
||||
)
|
||||
"--yes", "-y", action="store_true", default=False,
|
||||
help="Skip the disk-space confirmation prompt")
|
||||
|
||||
sessions_repair = sessions_subparsers.add_parser(
|
||||
"repair",
|
||||
@@ -320,19 +211,13 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"Recover a state.db whose schema is malformed (e.g. 'table "
|
||||
"messages_fts already exists'), which makes Desktop/Dashboard show "
|
||||
"no sessions. A backup is made first; sessions and messages are "
|
||||
"preserved and the FTS search index is rebuilt if needed."
|
||||
),
|
||||
)
|
||||
"preserved and the FTS search index is rebuilt if needed."))
|
||||
sessions_repair.add_argument(
|
||||
"--check-only",
|
||||
action="store_true",
|
||||
help="Only report whether the database opens cleanly; do not modify it",
|
||||
)
|
||||
"--check-only", action="store_true",
|
||||
help="Only report whether the database opens cleanly; do not modify it")
|
||||
sessions_repair.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Skip the timestamped backup copy (not recommended)",
|
||||
)
|
||||
"--no-backup", action="store_true",
|
||||
help="Skip the timestamped backup copy (not recommended)")
|
||||
|
||||
sessions_repair_routing = sessions_subparsers.add_parser(
|
||||
"repair-routing",
|
||||
@@ -345,14 +230,9 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"so the chat resumes an older session instead. Re-stamps each "
|
||||
"orphan from the keyed predecessor it continues, and only when "
|
||||
"that predecessor is unambiguous. Reports without touching the "
|
||||
"database unless --apply is given."
|
||||
),
|
||||
)
|
||||
"database unless --apply is given."))
|
||||
sessions_repair_routing.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Perform the adoptions (default: report only)",
|
||||
)
|
||||
"--apply", action="store_true", help="Perform the adoptions (default: report only)")
|
||||
sessions_repair_routing.add_argument(
|
||||
"--max-gap-seconds",
|
||||
type=float,
|
||||
@@ -360,9 +240,7 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
help=(
|
||||
"Window between a keyed predecessor's last activity and an "
|
||||
"orphan's start for them to count as the same conversation "
|
||||
"(default: 900)"
|
||||
),
|
||||
)
|
||||
"(default: 900)"))
|
||||
|
||||
sessions_recover = sessions_subparsers.add_parser(
|
||||
"recover",
|
||||
@@ -372,55 +250,34 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"source database and its WAL/SHM/rollback-journal sidecars are "
|
||||
"copied before SQLite opens anything. Canonical rows are rebuilt "
|
||||
"into a new output database; derived search indexes are recreated "
|
||||
"and the active database is never replaced automatically."
|
||||
),
|
||||
)
|
||||
"and the active database is never replaced automatically."))
|
||||
sessions_recover.add_argument(
|
||||
"--source",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Source state.db or preserved backup to inspect/recover",
|
||||
)
|
||||
"--source", type=Path, required=True,
|
||||
help="Source state.db or preserved backup to inspect/recover")
|
||||
sessions_recover.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
help="New recovery database path (required unless --inspect-only)",
|
||||
)
|
||||
"--output", type=Path, help="New recovery database path (required unless --inspect-only)")
|
||||
sessions_recover.add_argument(
|
||||
"--inspect-only",
|
||||
action="store_true",
|
||||
help="Only report canonical table readability; do not create an output database",
|
||||
)
|
||||
"--inspect-only", action="store_true",
|
||||
help="Only report canonical table readability; do not create an output database")
|
||||
sessions_recover.add_argument(
|
||||
"--work-dir",
|
||||
type=Path,
|
||||
help="Existing directory for the disposable source copy (defaults beside the output)",
|
||||
)
|
||||
"--work-dir", type=Path,
|
||||
help="Existing directory for the disposable source copy (defaults beside the output)")
|
||||
sessions_recover.add_argument(
|
||||
"--chunk-size",
|
||||
type=int,
|
||||
default=1000,
|
||||
help="Rows committed per recovery batch (default: 1000)",
|
||||
)
|
||||
"--chunk-size", type=int, default=1000,
|
||||
help="Rows committed per recovery batch (default: 1000)")
|
||||
sessions_recover.add_argument(
|
||||
"--allow-partial",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Best-effort salvage across damaged row ranges; the output remains "
|
||||
"separate and every skipped range is recorded"
|
||||
),
|
||||
)
|
||||
"separate and every skipped range is recorded"))
|
||||
sessions_recover.add_argument(
|
||||
"--report",
|
||||
type=Path,
|
||||
help="JSON report path (defaults to <output>.recovery.json)",
|
||||
)
|
||||
"--report", type=Path, help="JSON report path (defaults to <output>.recovery.json)")
|
||||
|
||||
sessions_subparsers.add_parser("stats", help="Show session store statistics")
|
||||
|
||||
sessions_rename = sessions_subparsers.add_parser(
|
||||
"rename", help="Set or change a session's title"
|
||||
)
|
||||
"rename", help="Set or change a session's title")
|
||||
sessions_rename.add_argument("session_id", help="Session ID to rename")
|
||||
sessions_rename.add_argument("title", nargs="+", help="New title for the session")
|
||||
|
||||
@@ -431,28 +288,19 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"Set the durable 'keep' flag on one or more sessions. Pinned "
|
||||
"sessions are exempt from the sessions.auto_archive stale sweep "
|
||||
"and always appear in listings. The same flag drives the Desktop "
|
||||
"sidebar's Pinned section — pin from either surface, both see it."
|
||||
),
|
||||
)
|
||||
"sidebar's Pinned section — pin from either surface, both see it."))
|
||||
sessions_pin.add_argument(
|
||||
"session_ids", nargs="+", help="Session ID(s) or unique prefix(es) to pin"
|
||||
)
|
||||
"session_ids", nargs="+", help="Session ID(s) or unique prefix(es) to pin")
|
||||
|
||||
sessions_unpin = sessions_subparsers.add_parser(
|
||||
"unpin", help="Remove the pin (durable keep flag) from session(s)"
|
||||
)
|
||||
"unpin", help="Remove the pin (durable keep flag) from session(s)")
|
||||
sessions_unpin.add_argument(
|
||||
"session_ids", nargs="+", help="Session ID(s) or unique prefix(es) to unpin"
|
||||
)
|
||||
"session_ids", nargs="+", help="Session ID(s) or unique prefix(es) to unpin")
|
||||
|
||||
sessions_pinned = sessions_subparsers.add_parser(
|
||||
"pinned", help="List pinned sessions"
|
||||
)
|
||||
sessions_pinned = sessions_subparsers.add_parser("pinned", help="List pinned sessions")
|
||||
sessions_pinned.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit machine-readable JSON (for backup/restore scripting)",
|
||||
)
|
||||
"--json", action="store_true",
|
||||
help="Emit machine-readable JSON (for backup/restore scripting)")
|
||||
|
||||
sessions_retitle = sessions_subparsers.add_parser(
|
||||
"retitle-skills",
|
||||
@@ -462,31 +310,17 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"message, which embeds the whole skill body — so the title "
|
||||
"describes the SKILL, not the request. This regenerates those "
|
||||
"titles from what the user actually typed. Lists what it would "
|
||||
"change unless --apply is passed."
|
||||
),
|
||||
)
|
||||
"change unless --apply is passed."))
|
||||
sessions_retitle.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Write the new titles (default: dry run)",
|
||||
)
|
||||
"--apply", action="store_true", help="Write the new titles (default: dry run)")
|
||||
sessions_retitle.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=200,
|
||||
help="Maximum sessions to examine (default: 200)",
|
||||
)
|
||||
"--limit", type=int, default=200, help="Maximum sessions to examine (default: 200)")
|
||||
|
||||
sessions_browse = sessions_subparsers.add_parser(
|
||||
"browse",
|
||||
help="Interactive session picker — browse, search, and resume sessions",
|
||||
)
|
||||
"browse", help="Interactive session picker — browse, search, and resume sessions")
|
||||
sessions_browse.add_argument("--source", help="Filter by source (cli, telegram, discord, etc.)")
|
||||
sessions_browse.add_argument(
|
||||
"--source", help="Filter by source (cli, telegram, discord, etc.)"
|
||||
)
|
||||
sessions_browse.add_argument(
|
||||
"--limit", type=int, default=500, help="Max sessions to load (default: 500)"
|
||||
)
|
||||
"--limit", type=int, default=500, help="Max sessions to load (default: 500)")
|
||||
|
||||
sessions_import = sessions_subparsers.add_parser(
|
||||
"import",
|
||||
@@ -495,20 +329,12 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None:
|
||||
"Pull a conversation started in Claude Code (~/.claude/projects) "
|
||||
"or Codex CLI (~/.codex/sessions) into the Hermes session store "
|
||||
"so it can be resumed with 'hermes --resume <id>'. The foreign "
|
||||
"files are only read, never modified."
|
||||
),
|
||||
)
|
||||
"files are only read, never modified."))
|
||||
sessions_import.add_argument(
|
||||
"--from",
|
||||
dest="from_source",
|
||||
choices=["claude", "codex"],
|
||||
help="Which tool to import from (default: pick across both)",
|
||||
)
|
||||
"--from", dest="from_source", choices=["claude", "codex"],
|
||||
help="Which tool to import from (default: pick across both)")
|
||||
sessions_import.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
help="Path to a specific session JSONL file (skips the picker)",
|
||||
)
|
||||
"path", nargs="?", help="Path to a specific session JSONL file (skips the picker)")
|
||||
|
||||
|
||||
# cmd_sessions lives in hermes_cli/sessions_cmd.py; the parser is threaded
|
||||
|
||||
@@ -12,49 +12,33 @@ def build_setup_parser(subparsers, *, cmd_setup: Callable) -> None:
|
||||
help="Interactive setup wizard",
|
||||
description="Configure Hermes Agent with an interactive wizard. "
|
||||
"Run a specific section: "
|
||||
"hermes setup model|tts|terminal|gateway|tools|telemetry|agent",
|
||||
)
|
||||
"hermes setup model|tts|terminal|gateway|tools|telemetry|agent")
|
||||
setup_parser.add_argument(
|
||||
"section",
|
||||
nargs="?",
|
||||
choices=[
|
||||
"model",
|
||||
"tts",
|
||||
"terminal",
|
||||
"gateway",
|
||||
"tools",
|
||||
"telemetry",
|
||||
"agent",
|
||||
],
|
||||
choices=["model", "tts", "terminal", "gateway", "tools", "telemetry", "agent"],
|
||||
default=None,
|
||||
help="Run a specific setup section instead of the full wizard",
|
||||
)
|
||||
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)",
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"--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.",
|
||||
)
|
||||
"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.",
|
||||
)
|
||||
"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.",
|
||||
)
|
||||
"Gateway. Skips the rest of the wizard.")
|
||||
setup_parser.set_defaults(func=cmd_setup)
|
||||
|
||||
@@ -8,42 +8,29 @@ 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",
|
||||
"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_trust = skills_subparsers.add_parser(
|
||||
"trust",
|
||||
help="Trust a project so its repo-local skills (./.hermes/skills, ./.agents/skills) load",
|
||||
)
|
||||
help="Trust a project so its repo-local skills (./.hermes/skills, ./.agents/skills) load")
|
||||
skills_trust.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Project root to trust (default: enclosing git checkout of cwd)",
|
||||
)
|
||||
"path", nargs="?", default=None,
|
||||
help="Project root to trust (default: enclosing git checkout of cwd)")
|
||||
|
||||
skills_untrust = skills_subparsers.add_parser(
|
||||
"untrust", help="Revoke project-skill trust for a repo"
|
||||
)
|
||||
"untrust", help="Revoke project-skill trust for a repo")
|
||||
skills_untrust.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Project root to untrust (default: enclosing git checkout of cwd)",
|
||||
)
|
||||
"path", nargs="?", default=None,
|
||||
help="Project root to untrust (default: enclosing git checkout of cwd)")
|
||||
|
||||
skills_browse = skills_subparsers.add_parser(
|
||||
"browse", help="Browse all available skills (paginated)"
|
||||
)
|
||||
"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(
|
||||
"--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)"
|
||||
)
|
||||
"--size", type=int, default=20, help="Results per page (default: 20)")
|
||||
skills_browse.add_argument(
|
||||
"--source",
|
||||
default="all",
|
||||
@@ -63,14 +50,10 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
"huggingface",
|
||||
"voltagent",
|
||||
"gstack",
|
||||
"minimax",
|
||||
],
|
||||
help="Filter by source or provider (e.g. nvidia, openai) (default: all)",
|
||||
)
|
||||
"minimax"],
|
||||
help="Filter by source or provider (e.g. nvidia, openai) (default: all)")
|
||||
|
||||
skills_search = skills_subparsers.add_parser(
|
||||
"search", help="Search skill registries"
|
||||
)
|
||||
skills_search = skills_subparsers.add_parser("search", help="Search skill registries")
|
||||
skills_search.add_argument("query", help="Search query")
|
||||
skills_search.add_argument(
|
||||
"--source",
|
||||
@@ -91,99 +74,64 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
"huggingface",
|
||||
"voltagent",
|
||||
"gstack",
|
||||
"minimax",
|
||||
],
|
||||
help="Filter by source or provider (e.g. nvidia, openai)",
|
||||
)
|
||||
"minimax"],
|
||||
help="Filter by source or provider (e.g. nvidia, openai)")
|
||||
skills_search.add_argument("--limit", type=int, default=25, help="Max results")
|
||||
skills_search.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output JSON instead of a table (full identifiers, scripting-friendly)",
|
||||
)
|
||||
"--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(
|
||||
"--category", default="", help="Category folder to install into"
|
||||
)
|
||||
skills_install.add_argument(
|
||||
"--name",
|
||||
default="",
|
||||
"--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"
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"-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"
|
||||
)
|
||||
"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("--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.",
|
||||
)
|
||||
"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)"
|
||||
)
|
||||
"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 = 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)",
|
||||
)
|
||||
"name", nargs="?", help="Specific skill to update (default: all outdated skills)")
|
||||
skills_update.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite skills you have edited locally (they are skipped by default)",
|
||||
)
|
||||
"--force", action="store_true",
|
||||
help="Overwrite skills you have edited locally (they are skipped by default)")
|
||||
|
||||
skills_audit = skills_subparsers.add_parser(
|
||||
"audit", help="Re-scan installed hub 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(
|
||||
"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)",
|
||||
)
|
||||
"--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"
|
||||
)
|
||||
"uninstall", help="Remove a hub-installed skill")
|
||||
skills_uninstall.add_argument("name", help="Skill name to remove")
|
||||
skills_uninstall.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt",
|
||||
)
|
||||
"-y", action="store_true", help="Skip confirmation prompt")
|
||||
|
||||
skills_reset = skills_subparsers.add_parser(
|
||||
"reset",
|
||||
@@ -191,23 +139,14 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
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."
|
||||
),
|
||||
)
|
||||
"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(
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"-y", action="store_true", help="Skip confirmation prompt when using --restore")
|
||||
|
||||
skills_list_modified = skills_subparsers.add_parser(
|
||||
"list-modified",
|
||||
@@ -216,14 +155,8 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
"Show the bundled skills whose local copy differs from the version last "
|
||||
"synced, i.e. the ones `hermes update` reports as user-modified and skips. "
|
||||
"Use `hermes skills diff <name>` to see changes and `hermes skills reset "
|
||||
"<name>` to resume updates."
|
||||
),
|
||||
)
|
||||
skills_list_modified.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output the list as JSON",
|
||||
)
|
||||
"<name>` to resume updates."))
|
||||
skills_list_modified.add_argument("--json", action="store_true", help="Output the list as JSON")
|
||||
|
||||
skills_diff = skills_subparsers.add_parser(
|
||||
"diff",
|
||||
@@ -231,12 +164,8 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
description=(
|
||||
"Print a unified diff between your local copy of a bundled skill and the "
|
||||
"current bundled (stock) version, so you can confirm what changed before "
|
||||
"running `hermes skills reset`."
|
||||
),
|
||||
)
|
||||
skills_diff.add_argument(
|
||||
"name", help="Skill name to diff (e.g. google-workspace)"
|
||||
)
|
||||
"running `hermes skills reset`."))
|
||||
skills_diff.add_argument("name", help="Skill name to diff (e.g. google-workspace)")
|
||||
|
||||
skills_opt_out = skills_subparsers.add_parser(
|
||||
"opt-out",
|
||||
@@ -246,34 +175,23 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
"`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)."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"-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."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--sync", action="store_true",
|
||||
help="Re-seed bundled skills immediately instead of waiting for update")
|
||||
|
||||
skills_repair_official = skills_subparsers.add_parser(
|
||||
"repair-official",
|
||||
@@ -282,50 +200,33 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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'"
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"-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 = 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"
|
||||
)
|
||||
"--to", default="github", choices=["github", "clawhub"], help="Target registry")
|
||||
skills_publish.add_argument(
|
||||
"--repo", default="", help="Target GitHub repo (e.g. openai/skills)"
|
||||
)
|
||||
"--repo", default="", help="Target GitHub repo (e.g. openai/skills)")
|
||||
|
||||
skills_snapshot = skills_subparsers.add_parser(
|
||||
"snapshot", help="Export/import skill configurations"
|
||||
)
|
||||
"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 = 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"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
"--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")
|
||||
@@ -337,8 +238,6 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
||||
|
||||
# config sub-action: interactive enable/disable
|
||||
skills_subparsers.add_parser(
|
||||
"config",
|
||||
help="Interactive skill configuration — enable/disable individual skills",
|
||||
)
|
||||
"config", help="Interactive skill configuration — enable/disable individual skills")
|
||||
|
||||
skills_parser.set_defaults(func=cmd_skills)
|
||||
|
||||
@@ -8,10 +8,8 @@ from typing import Callable
|
||||
def build_skin_parser(subparsers, *, cmd_skin: Callable) -> None:
|
||||
"""Attach the ``skin`` subcommand to ``subparsers``."""
|
||||
skin_parser = subparsers.add_parser(
|
||||
"skin",
|
||||
help="List, switch, and tweak skins",
|
||||
description="Manage Hermes skins. `set` tweaks one color of the active skin in place.",
|
||||
)
|
||||
"skin", help="List, switch, and tweak skins",
|
||||
description="Manage Hermes skins. `set` tweaks one color of the active skin in place.")
|
||||
skin_subparsers = skin_parser.add_subparsers(dest="skin_command")
|
||||
|
||||
skin_subparsers.add_parser("list", help="List available skins")
|
||||
@@ -21,8 +19,7 @@ def build_skin_parser(subparsers, *, cmd_skin: Callable) -> None:
|
||||
|
||||
# skin set — change ONE color of the active skin in place (bg untouched).
|
||||
skin_set = skin_subparsers.add_parser(
|
||||
"set", help="Set one color of the active skin (e.g. `skin set ui_tool '#00FFFF'`)"
|
||||
)
|
||||
"set", help="Set one color of the active skin (e.g. `skin set ui_tool '#00FFFF'`)")
|
||||
skin_set.add_argument("key", help="Color key (e.g. ui_tool, ui_accent, background)")
|
||||
skin_set.add_argument("value", help="Hex color (#rrggbb)")
|
||||
skin_set.add_argument("--skin", help="Target a specific skin instead of the active one")
|
||||
|
||||
@@ -8,10 +8,8 @@ from typing import Callable
|
||||
def build_slack_parser(subparsers, *, cmd_slack: Callable) -> None:
|
||||
"""Attach the ``slack`` subcommand to ``subparsers``."""
|
||||
slack_parser = subparsers.add_parser(
|
||||
"slack",
|
||||
help="Slack integration helpers (manifest generation, etc.)",
|
||||
description="Slack integration helpers for Hermes.",
|
||||
)
|
||||
"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",
|
||||
@@ -22,9 +20,7 @@ def build_slack_parser(subparsers, *, cmd_slack: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"Edit, then Save. Reinstall the app if Slack prompts for it."))
|
||||
slack_manifest.add_argument(
|
||||
"--write",
|
||||
nargs="?",
|
||||
@@ -32,40 +28,24 @@ def build_slack_parser(subparsers, *, cmd_slack: Callable) -> None:
|
||||
default=None,
|
||||
metavar="PATH",
|
||||
help="Write manifest to a file instead of stdout. With no PATH "
|
||||
"writes to $HERMES_HOME/slack-manifest.json.",
|
||||
)
|
||||
"writes to $HERMES_HOME/slack-manifest.json.")
|
||||
slack_manifest.add_argument("--name", default=None, help='Bot display name (default: "Hermes")')
|
||||
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.",
|
||||
)
|
||||
"--description", default=None, help="Bot description shown in Slack's app directory.")
|
||||
slack_long_description = slack_manifest.add_mutually_exclusive_group()
|
||||
slack_long_description.add_argument(
|
||||
"--long-description",
|
||||
default=None,
|
||||
metavar="TEXT",
|
||||
help="Set Slack's long app description (175-4,000 characters).",
|
||||
)
|
||||
"--long-description", default=None, metavar="TEXT",
|
||||
help="Set Slack's long app description (175-4,000 characters).")
|
||||
slack_long_description.add_argument(
|
||||
"--long-description-file",
|
||||
default=None,
|
||||
metavar="PATH",
|
||||
help=(
|
||||
"Read Slack's long app description from a UTF-8 text file "
|
||||
"(175-4,000 characters)."
|
||||
),
|
||||
)
|
||||
help=("Read Slack's long app description from a UTF-8 text file (175-4,000 characters)."))
|
||||
slack_manifest.add_argument(
|
||||
"--slashes-only",
|
||||
action="store_true",
|
||||
help="Emit only the features.slash_commands array (for merging "
|
||||
"into an existing manifest manually).",
|
||||
)
|
||||
"into an existing manifest manually).")
|
||||
slack_messaging = slack_manifest.add_mutually_exclusive_group()
|
||||
slack_messaging.add_argument(
|
||||
"--no-assistant",
|
||||
@@ -73,14 +53,12 @@ def build_slack_parser(subparsers, *, cmd_slack: Callable) -> None:
|
||||
help="Omit Slack AI Assistant mode (assistant_view, assistant:write "
|
||||
"scope, assistant_thread_* events). DMs then render as a flat chat "
|
||||
"where bare slash commands (/help, /new) work inline instead of "
|
||||
"Slack's Assistant thread pane.",
|
||||
)
|
||||
"Slack's Assistant thread pane.")
|
||||
slack_messaging.add_argument(
|
||||
"--agent-view",
|
||||
action="store_true",
|
||||
help="Emit Slack's Agent messaging experience (agent_view, "
|
||||
"app_home_opened + message.im) instead of the legacy assistant_view "
|
||||
"experience. This changes Slack's app messaging surface and cannot "
|
||||
"be reversed in Slack after applying the manifest.",
|
||||
)
|
||||
"be reversed in Slack after applying the manifest.")
|
||||
slack_parser.set_defaults(func=cmd_slack)
|
||||
|
||||
@@ -8,14 +8,10 @@ from typing import Callable
|
||||
def build_status_parser(subparsers, *, cmd_status: Callable) -> None:
|
||||
"""Attach the ``status`` subcommand to ``subparsers``."""
|
||||
status_parser = subparsers.add_parser(
|
||||
"status",
|
||||
help="Show status of all components",
|
||||
description="Display status of Hermes Agent components",
|
||||
)
|
||||
"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)"
|
||||
)
|
||||
"--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)"
|
||||
)
|
||||
"--deep", action="store_true", help="Run deep checks (may take longer)")
|
||||
status_parser.set_defaults(func=cmd_status)
|
||||
|
||||
@@ -37,23 +37,18 @@ def build_sync_parser(subparsers, *, cmd_sync: Callable) -> None:
|
||||
"Skill Sync keeps your skills with you. Personal sync moves your "
|
||||
"own skills between your devices; if you belong to an "
|
||||
"organisation, you also get its shared skills and can propose "
|
||||
"your own back to the team."
|
||||
),
|
||||
"your own back to the team."),
|
||||
epilog=(
|
||||
"Examples:\n"
|
||||
" hermes sync status what is synced, and from where\n"
|
||||
" hermes sync enable my-skill include a skill in your sync\n"
|
||||
" hermes sync now pull, then push\n"
|
||||
" hermes sync propose my-skill share a skill with your team\n"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
" hermes sync propose my-skill share a skill with your team\n"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sync_sub = sync_parser.add_subparsers(dest="sync_command")
|
||||
|
||||
sync_sub.add_parser("status", help="Show what is synced, and from where")
|
||||
sync_sub.add_parser(
|
||||
"pull", help="Pull your synced skills (and your organisation's)"
|
||||
)
|
||||
sync_sub.add_parser("pull", help="Pull your synced skills (and your organisation's)")
|
||||
sync_sub.add_parser("push", help="Push your opted-in skills")
|
||||
sync_sub.add_parser("now", help="Reconcile now: pull then push")
|
||||
|
||||
@@ -64,16 +59,13 @@ def build_sync_parser(subparsers, *, cmd_sync: Callable) -> None:
|
||||
disable.add_argument("skill", help="Skill name (frontmatter name / directory name)")
|
||||
|
||||
device = sync_sub.add_parser(
|
||||
"device",
|
||||
help="Show or set this device's label (shown in the sync console)",
|
||||
)
|
||||
"device", help="Show or set this device's label (shown in the sync console)")
|
||||
device.add_argument(
|
||||
"--name",
|
||||
dest="device_name",
|
||||
default=None,
|
||||
help="Set a human-friendly label for this device (e.g. \"Ben's Laptop\"). "
|
||||
"Omit to print the current label.",
|
||||
)
|
||||
"Omit to print the current label.")
|
||||
|
||||
# Org-shared skills. A member's submission becomes a proposal an admin
|
||||
# reviews; an admin's merges straight into the shared set. Accounts that
|
||||
@@ -85,15 +77,10 @@ def build_sync_parser(subparsers, *, cmd_sync: Callable) -> None:
|
||||
"Submit one of your skills to your organisation's shared set. If "
|
||||
"you are an admin it is added directly; otherwise it becomes a "
|
||||
"proposal for an admin to review. Accounts that aren't part of a "
|
||||
"shared organisation don't have this workflow."
|
||||
),
|
||||
)
|
||||
"shared organisation don't have this workflow."))
|
||||
propose.add_argument("name", help="Skill name to share")
|
||||
propose.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
help="Optional message describing the change",
|
||||
)
|
||||
"--message", default=None, help="Optional message describing the change")
|
||||
|
||||
sync_parser.set_defaults(func=cmd_sync)
|
||||
|
||||
@@ -14,60 +14,31 @@ def build_tools_parser(subparsers, *, cmd_tools: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"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 = 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",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--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 = 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",
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"--platform", default="cli", help="Platform to apply to (default: cli)")
|
||||
|
||||
# hermes tools post-setup <key>
|
||||
tools_postsetup_p = tools_sub.add_parser(
|
||||
@@ -80,12 +51,8 @@ def build_tools_parser(subparsers, *, cmd_tools: Callable) -> None:
|
||||
"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."
|
||||
),
|
||||
)
|
||||
"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)",
|
||||
)
|
||||
"post_setup_key", metavar="KEY",
|
||||
help="Post-setup hook key (e.g. agent_browser, camofox, kittentts)")
|
||||
tools_parser.set_defaults(func=cmd_tools)
|
||||
|
||||
@@ -8,32 +8,22 @@ from typing import Callable
|
||||
def build_uninstall_parser(subparsers, *, cmd_uninstall: Callable) -> None:
|
||||
"""Attach the ``uninstall`` subcommand to ``subparsers``."""
|
||||
uninstall_parser = subparsers.add_parser(
|
||||
"uninstall",
|
||||
help="Uninstall Hermes Agent",
|
||||
description="Remove Hermes Agent from your system. Can keep configs/data for reinstall.",
|
||||
)
|
||||
"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",
|
||||
)
|
||||
"--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",
|
||||
)
|
||||
"--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)",
|
||||
)
|
||||
"(used by the desktop app to gate uninstall options)")
|
||||
uninstall_parser.add_argument(
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation prompts"
|
||||
)
|
||||
"--yes", "-y", action="store_true", help="Skip confirmation prompts")
|
||||
uninstall_parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print what uninstall would remove without changing anything",
|
||||
)
|
||||
"--dry-run", action="store_true",
|
||||
help="Print what uninstall would remove without changing anything")
|
||||
uninstall_parser.set_defaults(func=cmd_uninstall)
|
||||
|
||||
@@ -8,22 +8,15 @@ from typing import Callable
|
||||
def build_update_parser(subparsers, *, cmd_update: Callable) -> None:
|
||||
"""Attach the ``update`` subcommand to ``subparsers``."""
|
||||
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", 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,
|
||||
"--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",
|
||||
)
|
||||
"--check", action="store_true", default=False,
|
||||
help="Check whether an update is available without installing anything")
|
||||
update_parser.add_argument(
|
||||
"--plan",
|
||||
action="store_true",
|
||||
@@ -32,26 +25,18 @@ def build_update_parser(subparsers, *, cmd_update: Callable) -> None:
|
||||
"Show the update plan and exit without changing anything: install "
|
||||
"kind (git/docker/nix), every running Hermes service across all "
|
||||
"profiles with its supervisor and running code version, and how "
|
||||
"each will be restarted. Read-only; safe on a live fleet."
|
||||
),
|
||||
)
|
||||
"each will be restarted. Read-only; safe on a live fleet."))
|
||||
update_parser.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
default=False,
|
||||
"--no-backup", action="store_true", default=False,
|
||||
help="Skip ALL pre-update backups for this run (both the quick state snapshot and the full zip; overrides updates.pre_update_backup)",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--backup",
|
||||
action="store_true",
|
||||
default=False,
|
||||
"--backup", action="store_true", default=False,
|
||||
help="Force a FULL pre-update backup (quick state snapshot + HERMES_HOME zip) for this run, regardless of updates.pre_update_backup",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
default=False,
|
||||
"-y", action="store_true", default=False,
|
||||
help="Run without blocking on prompts: accepts the config-migration and stash-restore prompts, skips the fork-upstream prompt without adding a remote. API-key entry is skipped; run 'hermes config migrate' separately for those.",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
@@ -63,9 +48,7 @@ def build_update_parser(subparsers, *, cmd_update: Callable) -> None:
|
||||
"changes are still stashed so the update can proceed, but they "
|
||||
"stay parked in git stash instead of being restored onto the "
|
||||
"updated code. Used by the desktop updater so local source edits "
|
||||
"never silently ride along across updates."
|
||||
),
|
||||
)
|
||||
"never silently ride along across updates."))
|
||||
update_parser.add_argument(
|
||||
"--branch",
|
||||
default=None,
|
||||
@@ -74,9 +57,7 @@ def build_update_parser(subparsers, *, cmd_update: Callable) -> None:
|
||||
"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)."
|
||||
),
|
||||
)
|
||||
"uncommitted changes)."))
|
||||
update_parser.add_argument(
|
||||
"--switch-branch",
|
||||
action="store_true",
|
||||
@@ -89,19 +70,13 @@ def build_update_parser(subparsers, *, cmd_update: Callable) -> None:
|
||||
"into its history. Use on long-lived feature branches where an "
|
||||
"update-driven merge commit would pollute the branch. No effect "
|
||||
"under the default strategy (switch), which already switches. "
|
||||
"Still refuses to touch a dirty tree."
|
||||
),
|
||||
)
|
||||
"Still refuses to touch a dirty tree."))
|
||||
update_parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
"--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. Does NOT bypass the venv-process guard (see --force-venv).",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--force-venv",
|
||||
action="store_true",
|
||||
default=False,
|
||||
"--force-venv", action="store_true", default=False,
|
||||
help="Windows: mutate the venv even while other processes are running from its interpreter (desktop backend, gateway, terminals). Those processes keep native .pyd files locked, so the dependency sync will likely fail partway and strand the install half-updated. Use only if you know the detected holders are false positives.",
|
||||
)
|
||||
update_parser.set_defaults(func=cmd_update)
|
||||
|
||||
@@ -23,58 +23,29 @@ def build_verify_parser(subparsers, *, cmd_verify: Callable) -> None:
|
||||
"Detect how the current project is built, tested, and started "
|
||||
"(or load the saved manifest at .hermes/environment.json), then "
|
||||
"run a verification pass: bootstrap -> build -> test -> start in "
|
||||
"background -> poll readiness -> teardown."
|
||||
),
|
||||
)
|
||||
"background -> poll readiness -> teardown."))
|
||||
verify_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Project root to verify (default: current directory)",
|
||||
)
|
||||
"path", nargs="?", default=None, help="Project root to verify (default: current directory)")
|
||||
verify_parser.add_argument(
|
||||
"--detect-only",
|
||||
action="store_true",
|
||||
help="Only detect and print the recipe as JSON; run nothing",
|
||||
)
|
||||
"--detect-only", action="store_true",
|
||||
help="Only detect and print the recipe as JSON; run nothing")
|
||||
verify_parser.add_argument(
|
||||
"--save",
|
||||
action="store_true",
|
||||
help="Save the recipe as .hermes/environment.json in the project",
|
||||
)
|
||||
"--save", action="store_true",
|
||||
help="Save the recipe as .hermes/environment.json in the project")
|
||||
verify_parser.add_argument(
|
||||
"--skip-start",
|
||||
action="store_true",
|
||||
help="Run command phases but skip starting the app / readiness poll",
|
||||
)
|
||||
"--skip-start", action="store_true",
|
||||
help="Run command phases but skip starting the app / readiness poll")
|
||||
verify_parser.add_argument(
|
||||
"--phase",
|
||||
action="append",
|
||||
choices=["bootstrap", "build", "test", "start"],
|
||||
default=None,
|
||||
help="Run only the given phase(s); repeatable",
|
||||
)
|
||||
"--phase", action="append", choices=["bootstrap", "build", "test", "start"], default=None,
|
||||
help="Run only the given phase(s); repeatable")
|
||||
verify_parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Override the port used for the readiness poll",
|
||||
)
|
||||
"--port", type=int, default=None, help="Override the port used for the readiness poll")
|
||||
verify_parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=DEFAULT_PHASE_TIMEOUT,
|
||||
help=f"Per-phase timeout in seconds (default: {DEFAULT_PHASE_TIMEOUT:.0f})",
|
||||
)
|
||||
"--timeout", type=float, default=DEFAULT_PHASE_TIMEOUT,
|
||||
help=f"Per-phase timeout in seconds (default: {DEFAULT_PHASE_TIMEOUT:.0f})")
|
||||
verify_parser.add_argument(
|
||||
"--ready-timeout",
|
||||
type=float,
|
||||
default=DEFAULT_READY_TIMEOUT,
|
||||
help=f"Readiness poll timeout in seconds (default: {DEFAULT_READY_TIMEOUT:.0f})",
|
||||
)
|
||||
"--ready-timeout", type=float, default=DEFAULT_READY_TIMEOUT,
|
||||
help=f"Readiness poll timeout in seconds (default: {DEFAULT_READY_TIMEOUT:.0f})")
|
||||
verify_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit a machine-readable JSON result",
|
||||
)
|
||||
"--json", action="store_true", help="Emit a machine-readable JSON result")
|
||||
verify_parser.set_defaults(func=cmd_verify)
|
||||
|
||||
@@ -8,69 +8,45 @@ from typing import Callable
|
||||
def build_webhook_parser(subparsers, *, cmd_webhook: Callable) -> None:
|
||||
"""Attach the ``webhook`` subcommand to ``subparsers``."""
|
||||
webhook_parser = subparsers.add_parser(
|
||||
"webhook",
|
||||
help="Manage dynamic webhook subscriptions",
|
||||
"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"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
"--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(
|
||||
"--skills", default="", help="Comma-separated skill names to load"
|
||||
)
|
||||
"--deliver", default="log", help="Delivery target: log, telegram, discord, slack, etc.")
|
||||
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)"
|
||||
)
|
||||
"--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').",
|
||||
)
|
||||
"(not 'log').")
|
||||
wh_sub.add_argument(
|
||||
"--script",
|
||||
default="",
|
||||
help="Filter/transform script under ~/.hermes/scripts/. The route "
|
||||
"payload is passed as JSON on stdin; empty stdout, [SILENT], or a "
|
||||
"nonzero exit code ignores the webhook.",
|
||||
)
|
||||
"nonzero exit code ignores the webhook.")
|
||||
|
||||
webhook_subparsers.add_parser(
|
||||
"list", aliases=["ls"], help="List all dynamic subscriptions"
|
||||
)
|
||||
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 = 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 = 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)"
|
||||
)
|
||||
"--payload", default="", help="JSON payload to send (default: test payload)")
|
||||
|
||||
webhook_parser.set_defaults(func=cmd_webhook)
|
||||
|
||||
@@ -8,10 +8,8 @@ from typing import Callable
|
||||
def build_whatsapp_parser(subparsers, *, cmd_whatsapp: Callable) -> None:
|
||||
"""Attach the ``whatsapp`` subcommand to ``subparsers``."""
|
||||
whatsapp_parser = subparsers.add_parser(
|
||||
"whatsapp",
|
||||
help="Set up WhatsApp integration",
|
||||
description="Configure WhatsApp and pair via QR code",
|
||||
)
|
||||
"whatsapp", help="Set up WhatsApp integration",
|
||||
description="Configure WhatsApp and pair via QR code")
|
||||
whatsapp_parser.set_defaults(func=cmd_whatsapp)
|
||||
|
||||
|
||||
@@ -24,7 +22,5 @@ def build_whatsapp_cloud_parser(subparsers, *, cmd_whatsapp_cloud: Callable) ->
|
||||
"Configure the official Meta WhatsApp Business Cloud API "
|
||||
"adapter (Business account required, public webhook URL "
|
||||
"required). Distinct from `hermes whatsapp` which sets up "
|
||||
"the Baileys bridge for personal accounts."
|
||||
),
|
||||
)
|
||||
"the Baileys bridge for personal accounts."))
|
||||
whatsapp_cloud_parser.set_defaults(func=cmd_whatsapp_cloud)
|
||||
|
||||
@@ -13,33 +13,23 @@ def build_worktree_parser(subparsers) -> None:
|
||||
"accumulate. Never deletes uncommitted tracked changes, unique "
|
||||
"unpushed commits, or in-use trees; untracked-only scratch is "
|
||||
"archived to ~/.hermes/archive/worktree-prune/ before removal. See: "
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/cli#worktree-cleanup"
|
||||
),
|
||||
)
|
||||
"https://hermes-agent.nousresearch.com/docs/user-guide/cli#worktree-cleanup"))
|
||||
worktree_subparsers = worktree_parser.add_subparsers(dest="worktree_action")
|
||||
worktree_list = worktree_subparsers.add_parser(
|
||||
"list",
|
||||
aliases=["ls", "audit"],
|
||||
help="Classify every tree: age, size, verdict, reason (default action)",
|
||||
)
|
||||
"list", aliases=["ls", "audit"],
|
||||
help="Classify every tree: age, size, verdict, reason (default action)")
|
||||
worktree_list.add_argument("--repo", help="Repo root (default: current repo)")
|
||||
worktree_prune = worktree_subparsers.add_parser(
|
||||
"prune",
|
||||
help="Remove safe trees and delete fully-merged local branches",
|
||||
)
|
||||
"prune", help="Remove safe trees and delete fully-merged local branches")
|
||||
worktree_prune.add_argument("--repo", help="Repo root (default: current repo)")
|
||||
worktree_prune.add_argument(
|
||||
"--dry-run", action="store_true",
|
||||
help="Show the plan without changing anything",
|
||||
)
|
||||
"--dry-run", action="store_true", help="Show the plan without changing anything")
|
||||
worktree_prune.add_argument(
|
||||
"--trees-only", action="store_true",
|
||||
help="Only remove worktrees; leave local branches alone",
|
||||
)
|
||||
help="Only remove worktrees; leave local branches alone")
|
||||
worktree_prune.add_argument(
|
||||
"--branches-only", action="store_true",
|
||||
help="Only delete merged local branches; leave worktrees alone",
|
||||
)
|
||||
help="Only delete merged local branches; leave worktrees alone")
|
||||
|
||||
def _dispatch_worktree(_args):
|
||||
from hermes_cli.worktree_cmd import cmd_worktree
|
||||
|
||||
Reference in New Issue
Block a user