mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
refactor(cli): extract profile + gateway/proxy parsers into hermes_cli/subcommands/
Follow-on to the cron extraction in the same Phase 2 PR. Same pattern: per-group build_<name>_parser() functions with injected handlers, no main import. - subcommands/profile.py: build_profile_parser (190-line block out of main()). - subcommands/gateway.py: build_gateway_parser (gateway + proxy, 238-line block; they shared one inline section). Imports argparse for SUPPRESS defaults. - main(): two more inline blocks become single builder calls. Behavior-neutral: 'profile [sub] --help' and 'gateway/proxy [sub] --help' byte-identical to pre-extraction (diff-verified). main() now 2723 LOC (was 3297 at Phase 2 start); add_parser calls in main.py 179 -> 141. Validation: tests/hermes_cli/ 6476 passed / 0 failed under per-file process isolation; new builder unit tests cover subactions, aliases, dispatch, flags.
This commit is contained in:
parent
b2e6053243
commit
4da45e8727
4 changed files with 548 additions and 424 deletions
|
|
@ -264,6 +264,8 @@ from typing import Optional
|
|||
|
||||
from hermes_cli.subcommands._shared import add_accept_hooks_flag as _add_accept_hooks_flag
|
||||
from hermes_cli.subcommands.cron import build_cron_parser
|
||||
from hermes_cli.subcommands.gateway import build_gateway_parser
|
||||
from hermes_cli.subcommands.profile import build_profile_parser
|
||||
|
||||
|
||||
def _require_tty(command_name: str) -> None:
|
||||
|
|
@ -13040,243 +13042,9 @@ def main():
|
|||
migrate_parser.set_defaults(func=cmd_migrate)
|
||||
|
||||
# =========================================================================
|
||||
# gateway command
|
||||
# gateway + proxy commands (parsers built in hermes_cli/subcommands/gateway.py)
|
||||
# =========================================================================
|
||||
gateway_parser = subparsers.add_parser(
|
||||
"gateway",
|
||||
help="Messaging gateway management",
|
||||
description="Manage the messaging gateway (Telegram, Discord, WhatsApp, Weixin, and more)",
|
||||
)
|
||||
gateway_subparsers = gateway_parser.add_subparsers(dest="gateway_command")
|
||||
|
||||
# gateway run (default)
|
||||
gateway_run = gateway_subparsers.add_parser(
|
||||
"run", help="Run gateway in foreground (recommended for WSL, Docker, Termux)"
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Increase stderr log verbosity (-v=INFO, -vv=DEBUG)",
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Suppress all stderr log output"
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"--replace",
|
||||
action="store_true",
|
||||
help="Replace any existing gateway instance (useful for systemd)",
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"--no-supervise",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Inside the s6-overlay Docker image, normally `gateway run` is "
|
||||
"automatically redirected to the supervised s6 service (so the "
|
||||
"gateway gets auto-restart on crash, plus a supervised dashboard "
|
||||
"if HERMES_DASHBOARD is set). Pass --no-supervise to opt out and "
|
||||
"get the historical pre-s6 foreground behavior: the gateway is "
|
||||
"the container's main process and the container exits with the "
|
||||
"gateway's exit code. No effect outside an s6 container."
|
||||
),
|
||||
)
|
||||
_add_accept_hooks_flag(gateway_run)
|
||||
_add_accept_hooks_flag(gateway_parser)
|
||||
|
||||
# gateway start
|
||||
gateway_start = gateway_subparsers.add_parser(
|
||||
"start", help="Start the installed systemd/launchd background service"
|
||||
)
|
||||
gateway_start.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
gateway_start.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Kill ALL stale gateway processes across all profiles before starting",
|
||||
)
|
||||
|
||||
# gateway stop
|
||||
gateway_stop = gateway_subparsers.add_parser("stop", help="Stop gateway service")
|
||||
gateway_stop.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
gateway_stop.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Stop ALL gateway processes across all profiles",
|
||||
)
|
||||
|
||||
# gateway restart
|
||||
gateway_restart = gateway_subparsers.add_parser(
|
||||
"restart", help="Restart gateway service"
|
||||
)
|
||||
gateway_restart.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
gateway_restart.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Kill ALL gateway processes across all profiles before restarting",
|
||||
)
|
||||
|
||||
# gateway status
|
||||
gateway_status = gateway_subparsers.add_parser("status", help="Show gateway status")
|
||||
gateway_status.add_argument("--deep", action="store_true", help="Deep status check")
|
||||
gateway_status.add_argument(
|
||||
"-l",
|
||||
"--full",
|
||||
action="store_true",
|
||||
help="Show full, untruncated service/log output where supported",
|
||||
)
|
||||
gateway_status.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
|
||||
# gateway install
|
||||
gateway_install = gateway_subparsers.add_parser(
|
||||
"install", help="Install gateway as a systemd/launchd background service"
|
||||
)
|
||||
gateway_install.add_argument("--force", action="store_true", help="Force reinstall")
|
||||
gateway_install.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Install as a Linux system-level service (starts at boot)",
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--run-as-user",
|
||||
dest="run_as_user",
|
||||
help="User account the Linux system service should run as",
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--start-now",
|
||||
dest="start_now",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--no-start-now",
|
||||
dest="start_now",
|
||||
action="store_false",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--start-on-login",
|
||||
dest="start_on_login",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--no-start-on-login",
|
||||
dest="start_on_login",
|
||||
action="store_false",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--elevated-handoff",
|
||||
dest="elevated_handoff",
|
||||
action="store_true",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
# gateway uninstall
|
||||
gateway_uninstall = gateway_subparsers.add_parser(
|
||||
"uninstall", help="Uninstall gateway service"
|
||||
)
|
||||
gateway_uninstall.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
|
||||
# gateway list
|
||||
gateway_subparsers.add_parser("list", help="List all profiles and their gateway status")
|
||||
|
||||
# gateway setup
|
||||
gateway_subparsers.add_parser("setup", help="Configure messaging platforms")
|
||||
|
||||
# gateway migrate-legacy
|
||||
gateway_migrate_legacy = gateway_subparsers.add_parser(
|
||||
"migrate-legacy",
|
||||
help="Remove legacy hermes.service units from pre-rename installs",
|
||||
description=(
|
||||
"Stop, disable, and remove legacy Hermes gateway unit files "
|
||||
"(e.g. hermes.service) left over from older installs. Profile "
|
||||
"units (hermes-gateway-<profile>.service) and unrelated "
|
||||
"third-party services are never touched."
|
||||
),
|
||||
)
|
||||
gateway_migrate_legacy.add_argument(
|
||||
"--dry-run",
|
||||
dest="dry_run",
|
||||
action="store_true",
|
||||
help="List what would be removed without doing it",
|
||||
)
|
||||
gateway_migrate_legacy.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
dest="yes",
|
||||
action="store_true",
|
||||
help="Skip the confirmation prompt",
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# proxy command — local OpenAI-compatible proxy that attaches the user's
|
||||
# OAuth-authenticated provider credentials to outbound requests. Lets
|
||||
# external apps (OpenViking, Karakeep, Open WebUI, ...) ride a logged-in
|
||||
# subscription without copy-pasting static API keys.
|
||||
# =========================================================================
|
||||
proxy_parser = subparsers.add_parser(
|
||||
"proxy",
|
||||
help="Local OpenAI-compatible proxy to OAuth providers",
|
||||
description=(
|
||||
"Run a local HTTP server that forwards OpenAI-compatible requests "
|
||||
"to an OAuth-authenticated provider (e.g. Nous Portal). External "
|
||||
"apps can point at the proxy with any bearer token; the proxy "
|
||||
"attaches your real credentials."
|
||||
),
|
||||
)
|
||||
proxy_subparsers = proxy_parser.add_subparsers(dest="proxy_command")
|
||||
|
||||
proxy_start = proxy_subparsers.add_parser(
|
||||
"start", help="Run the proxy in the foreground"
|
||||
)
|
||||
proxy_start.add_argument(
|
||||
"--provider",
|
||||
default="nous",
|
||||
help="Upstream provider: nous or xai (default: nous). See `hermes proxy providers`.",
|
||||
)
|
||||
proxy_start.add_argument(
|
||||
"--host",
|
||||
default=None,
|
||||
help="Bind address (default: 127.0.0.1). Use 0.0.0.0 to expose on LAN.",
|
||||
)
|
||||
proxy_start.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Bind port (default: 8645)",
|
||||
)
|
||||
|
||||
proxy_subparsers.add_parser(
|
||||
"status", help="Show which proxy upstreams are ready"
|
||||
)
|
||||
proxy_subparsers.add_parser(
|
||||
"providers", help="List available proxy upstream providers"
|
||||
)
|
||||
proxy_parser.set_defaults(func=cmd_proxy)
|
||||
gateway_parser.set_defaults(func=cmd_gateway)
|
||||
build_gateway_parser(subparsers, cmd_gateway=cmd_gateway, cmd_proxy=cmd_proxy)
|
||||
|
||||
# =========================================================================
|
||||
# lsp command
|
||||
|
|
@ -15393,195 +15161,9 @@ Examples:
|
|||
acp_parser.set_defaults(func=cmd_acp)
|
||||
|
||||
# =========================================================================
|
||||
# profile command
|
||||
# profile command (parser built in hermes_cli/subcommands/profile.py)
|
||||
# =========================================================================
|
||||
profile_parser = subparsers.add_parser(
|
||||
"profile",
|
||||
help="Manage profiles — multiple isolated Hermes instances",
|
||||
)
|
||||
profile_subparsers = profile_parser.add_subparsers(dest="profile_action")
|
||||
|
||||
profile_subparsers.add_parser("list", help="List all profiles")
|
||||
profile_use = profile_subparsers.add_parser(
|
||||
"use", help="Set sticky default profile"
|
||||
)
|
||||
profile_use.add_argument("profile_name", help="Profile name (or 'default')")
|
||||
|
||||
profile_create = profile_subparsers.add_parser(
|
||||
"create", help="Create a new profile"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"profile_name", help="Profile name (lowercase, alphanumeric)"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--clone",
|
||||
action="store_true",
|
||||
help="Copy config.yaml, .env, SOUL.md from active profile",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--clone-all",
|
||||
action="store_true",
|
||||
help="Full copy of active profile (all state)",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--clone-from",
|
||||
metavar="SOURCE",
|
||||
help="Source profile to clone from (default: active)",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--no-alias", action="store_true", help="Skip wrapper script creation"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--no-skills",
|
||||
action="store_true",
|
||||
help="Create an empty profile with no bundled skills (opts out of `hermes update` skill sync)",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--description",
|
||||
default=None,
|
||||
help="One- or two-sentence description of what this profile is good at. "
|
||||
"Used by the kanban decomposer to route tasks based on role instead "
|
||||
"of profile name alone. Skip and add later via `hermes profile describe`.",
|
||||
)
|
||||
|
||||
profile_delete = profile_subparsers.add_parser("delete", help="Delete a profile")
|
||||
profile_delete.add_argument("profile_name", help="Profile to delete")
|
||||
profile_delete.add_argument(
|
||||
"-y", "--yes", action="store_true", help="Skip confirmation prompt"
|
||||
)
|
||||
|
||||
profile_describe = profile_subparsers.add_parser(
|
||||
"describe",
|
||||
help="Read or set a profile's description (used by the kanban orchestrator)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"profile_name",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Profile to describe (omit + use --all --auto to sweep)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--text",
|
||||
default=None,
|
||||
help="Set description to this exact text (overwrites any existing description)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--auto",
|
||||
action="store_true",
|
||||
help="Auto-generate description via the auxiliary LLM "
|
||||
"(uses auxiliary.profile_describer)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="With --auto, replace user-authored descriptions too (default: only "
|
||||
"fill in missing or previously-auto descriptions)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--all",
|
||||
dest="all_missing",
|
||||
action="store_true",
|
||||
help="With --auto, run on every profile missing a description",
|
||||
)
|
||||
|
||||
profile_show = profile_subparsers.add_parser("show", help="Show profile details")
|
||||
profile_show.add_argument("profile_name", help="Profile to show")
|
||||
|
||||
profile_alias = profile_subparsers.add_parser(
|
||||
"alias", help="Manage wrapper scripts"
|
||||
)
|
||||
profile_alias.add_argument("profile_name", help="Profile name")
|
||||
profile_alias.add_argument(
|
||||
"--remove", action="store_true", help="Remove the wrapper script"
|
||||
)
|
||||
profile_alias.add_argument(
|
||||
"--name",
|
||||
dest="alias_name",
|
||||
metavar="NAME",
|
||||
help="Custom alias name (default: profile name)",
|
||||
)
|
||||
|
||||
profile_rename = profile_subparsers.add_parser("rename", help="Rename a profile")
|
||||
profile_rename.add_argument("old_name", help="Current profile name")
|
||||
profile_rename.add_argument("new_name", help="New profile name")
|
||||
|
||||
profile_export = profile_subparsers.add_parser(
|
||||
"export", help="Export a profile to archive"
|
||||
)
|
||||
profile_export.add_argument("profile_name", help="Profile to export")
|
||||
profile_export.add_argument(
|
||||
"-o", "--output", default=None, help="Output file (default: <name>.tar.gz)"
|
||||
)
|
||||
|
||||
profile_import = profile_subparsers.add_parser(
|
||||
"import", help="Import a profile from archive"
|
||||
)
|
||||
profile_import.add_argument("archive", help="Path to .tar.gz archive")
|
||||
profile_import.add_argument(
|
||||
"--name",
|
||||
dest="import_name",
|
||||
metavar="NAME",
|
||||
help="Profile name (default: inferred from archive)",
|
||||
)
|
||||
|
||||
# ---------- Distribution subcommands (issue #20456) ----------
|
||||
profile_install = profile_subparsers.add_parser(
|
||||
"install",
|
||||
help="Install a profile distribution from a git URL or local directory",
|
||||
description=(
|
||||
"Install a Hermes profile distribution. SOURCE can be a git URL "
|
||||
"(github.com/user/repo, https://..., git@...) or a local "
|
||||
"directory containing distribution.yaml at its root."
|
||||
),
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"source",
|
||||
help="Distribution source (git URL or local directory)",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"--name", dest="install_name", metavar="NAME",
|
||||
help="Override profile name (default: read from manifest)",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"--alias", action="store_true",
|
||||
help="Create a shell wrapper alias for the installed profile",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"--force", action="store_true",
|
||||
help="Overwrite an existing profile of the same name (user data preserved)",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"-y", "--yes", action="store_true",
|
||||
help="Skip manifest preview confirmation",
|
||||
)
|
||||
|
||||
profile_update = profile_subparsers.add_parser(
|
||||
"update",
|
||||
help="Re-pull a distribution and apply updates (user data preserved)",
|
||||
description=(
|
||||
"Fetch the distribution from its recorded source and overwrite "
|
||||
"distribution-owned files (SOUL.md, skills/, cron/, mcp.json). "
|
||||
"User data (memories, sessions, auth, .env) is never touched. "
|
||||
"config.yaml is preserved unless --force-config is passed."
|
||||
),
|
||||
)
|
||||
profile_update.add_argument("profile_name", help="Profile to update")
|
||||
profile_update.add_argument(
|
||||
"--force-config", action="store_true",
|
||||
help="Also overwrite config.yaml (normally preserved to keep user overrides)",
|
||||
)
|
||||
profile_update.add_argument(
|
||||
"-y", "--yes", action="store_true",
|
||||
help="Skip confirmation",
|
||||
)
|
||||
|
||||
profile_info = profile_subparsers.add_parser(
|
||||
"info",
|
||||
help="Show a profile's distribution manifest (version, requirements, source)",
|
||||
)
|
||||
profile_info.add_argument("profile_name", help="Profile to inspect")
|
||||
|
||||
profile_parser.set_defaults(func=cmd_profile)
|
||||
build_profile_parser(subparsers, cmd_profile=cmd_profile)
|
||||
|
||||
# =========================================================================
|
||||
# completion command
|
||||
|
|
|
|||
256
hermes_cli/subcommands/gateway.py
Normal file
256
hermes_cli/subcommands/gateway.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
"""``hermes gateway`` and ``hermes proxy`` subcommand parsers.
|
||||
|
||||
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
|
||||
Both parsers are built together because they shared one inline block (the
|
||||
``gateway`` section also defined ``proxy``). Handlers injected to avoid
|
||||
importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Callable
|
||||
|
||||
from hermes_cli.subcommands._shared import add_accept_hooks_flag
|
||||
|
||||
|
||||
def build_gateway_parser(subparsers, *, cmd_gateway: Callable, cmd_proxy: Callable) -> None:
|
||||
"""Attach the ``gateway`` and ``proxy`` subcommands to ``subparsers``."""
|
||||
# =========================================================================
|
||||
# gateway command
|
||||
# =========================================================================
|
||||
gateway_parser = subparsers.add_parser(
|
||||
"gateway",
|
||||
help="Messaging gateway management",
|
||||
description="Manage the messaging gateway (Telegram, Discord, WhatsApp, Weixin, and more)",
|
||||
)
|
||||
gateway_subparsers = gateway_parser.add_subparsers(dest="gateway_command")
|
||||
|
||||
# gateway run (default)
|
||||
gateway_run = gateway_subparsers.add_parser(
|
||||
"run", help="Run gateway in foreground (recommended for WSL, Docker, Termux)"
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Increase stderr log verbosity (-v=INFO, -vv=DEBUG)",
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Suppress all stderr log output"
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"--replace",
|
||||
action="store_true",
|
||||
help="Replace any existing gateway instance (useful for systemd)",
|
||||
)
|
||||
gateway_run.add_argument(
|
||||
"--no-supervise",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Inside the s6-overlay Docker image, normally `gateway run` is "
|
||||
"automatically redirected to the supervised s6 service (so the "
|
||||
"gateway gets auto-restart on crash, plus a supervised dashboard "
|
||||
"if HERMES_DASHBOARD is set). Pass --no-supervise to opt out and "
|
||||
"get the historical pre-s6 foreground behavior: the gateway is "
|
||||
"the container's main process and the container exits with the "
|
||||
"gateway's exit code. No effect outside an s6 container."
|
||||
),
|
||||
)
|
||||
add_accept_hooks_flag(gateway_run)
|
||||
add_accept_hooks_flag(gateway_parser)
|
||||
|
||||
# gateway start
|
||||
gateway_start = gateway_subparsers.add_parser(
|
||||
"start", help="Start the installed systemd/launchd background service"
|
||||
)
|
||||
gateway_start.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
gateway_start.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Kill ALL stale gateway processes across all profiles before starting",
|
||||
)
|
||||
|
||||
# gateway stop
|
||||
gateway_stop = gateway_subparsers.add_parser("stop", help="Stop gateway service")
|
||||
gateway_stop.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
gateway_stop.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Stop ALL gateway processes across all profiles",
|
||||
)
|
||||
|
||||
# gateway restart
|
||||
gateway_restart = gateway_subparsers.add_parser(
|
||||
"restart", help="Restart gateway service"
|
||||
)
|
||||
gateway_restart.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
gateway_restart.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Kill ALL gateway processes across all profiles before restarting",
|
||||
)
|
||||
|
||||
# gateway status
|
||||
gateway_status = gateway_subparsers.add_parser("status", help="Show gateway status")
|
||||
gateway_status.add_argument("--deep", action="store_true", help="Deep status check")
|
||||
gateway_status.add_argument(
|
||||
"-l",
|
||||
"--full",
|
||||
action="store_true",
|
||||
help="Show full, untruncated service/log output where supported",
|
||||
)
|
||||
gateway_status.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
|
||||
# gateway install
|
||||
gateway_install = gateway_subparsers.add_parser(
|
||||
"install", help="Install gateway as a systemd/launchd background service"
|
||||
)
|
||||
gateway_install.add_argument("--force", action="store_true", help="Force reinstall")
|
||||
gateway_install.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Install as a Linux system-level service (starts at boot)",
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--run-as-user",
|
||||
dest="run_as_user",
|
||||
help="User account the Linux system service should run as",
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--start-now",
|
||||
dest="start_now",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--no-start-now",
|
||||
dest="start_now",
|
||||
action="store_false",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--start-on-login",
|
||||
dest="start_on_login",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--no-start-on-login",
|
||||
dest="start_on_login",
|
||||
action="store_false",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
gateway_install.add_argument(
|
||||
"--elevated-handoff",
|
||||
dest="elevated_handoff",
|
||||
action="store_true",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
# gateway uninstall
|
||||
gateway_uninstall = gateway_subparsers.add_parser(
|
||||
"uninstall", help="Uninstall gateway service"
|
||||
)
|
||||
gateway_uninstall.add_argument(
|
||||
"--system",
|
||||
action="store_true",
|
||||
help="Target the Linux system-level gateway service",
|
||||
)
|
||||
|
||||
# gateway list
|
||||
gateway_subparsers.add_parser("list", help="List all profiles and their gateway status")
|
||||
|
||||
# gateway setup
|
||||
gateway_subparsers.add_parser("setup", help="Configure messaging platforms")
|
||||
|
||||
# gateway migrate-legacy
|
||||
gateway_migrate_legacy = gateway_subparsers.add_parser(
|
||||
"migrate-legacy",
|
||||
help="Remove legacy hermes.service units from pre-rename installs",
|
||||
description=(
|
||||
"Stop, disable, and remove legacy Hermes gateway unit files "
|
||||
"(e.g. hermes.service) left over from older installs. Profile "
|
||||
"units (hermes-gateway-<profile>.service) and unrelated "
|
||||
"third-party services are never touched."
|
||||
),
|
||||
)
|
||||
gateway_migrate_legacy.add_argument(
|
||||
"--dry-run",
|
||||
dest="dry_run",
|
||||
action="store_true",
|
||||
help="List what would be removed without doing it",
|
||||
)
|
||||
gateway_migrate_legacy.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
dest="yes",
|
||||
action="store_true",
|
||||
help="Skip the confirmation prompt",
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# proxy command — local OpenAI-compatible proxy that attaches the user's
|
||||
# OAuth-authenticated provider credentials to outbound requests. Lets
|
||||
# external apps (OpenViking, Karakeep, Open WebUI, ...) ride a logged-in
|
||||
# subscription without copy-pasting static API keys.
|
||||
# =========================================================================
|
||||
proxy_parser = subparsers.add_parser(
|
||||
"proxy",
|
||||
help="Local OpenAI-compatible proxy to OAuth providers",
|
||||
description=(
|
||||
"Run a local HTTP server that forwards OpenAI-compatible requests "
|
||||
"to an OAuth-authenticated provider (e.g. Nous Portal). External "
|
||||
"apps can point at the proxy with any bearer token; the proxy "
|
||||
"attaches your real credentials."
|
||||
),
|
||||
)
|
||||
proxy_subparsers = proxy_parser.add_subparsers(dest="proxy_command")
|
||||
|
||||
proxy_start = proxy_subparsers.add_parser(
|
||||
"start", help="Run the proxy in the foreground"
|
||||
)
|
||||
proxy_start.add_argument(
|
||||
"--provider",
|
||||
default="nous",
|
||||
help="Upstream provider: nous or xai (default: nous). See `hermes proxy providers`.",
|
||||
)
|
||||
proxy_start.add_argument(
|
||||
"--host",
|
||||
default=None,
|
||||
help="Bind address (default: 127.0.0.1). Use 0.0.0.0 to expose on LAN.",
|
||||
)
|
||||
proxy_start.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Bind port (default: 8645)",
|
||||
)
|
||||
|
||||
proxy_subparsers.add_parser(
|
||||
"status", help="Show which proxy upstreams are ready"
|
||||
)
|
||||
proxy_subparsers.add_parser(
|
||||
"providers", help="List available proxy upstream providers"
|
||||
)
|
||||
proxy_parser.set_defaults(func=cmd_proxy)
|
||||
gateway_parser.set_defaults(func=cmd_gateway)
|
||||
203
hermes_cli/subcommands/profile.py
Normal file
203
hermes_cli/subcommands/profile.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
"""``hermes profile`` subcommand parser.
|
||||
|
||||
Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2).
|
||||
Handler injected to avoid importing ``main``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None:
|
||||
"""Attach the ``profile`` subcommand to ``subparsers``."""
|
||||
# =========================================================================
|
||||
# profile command
|
||||
# =========================================================================
|
||||
profile_parser = subparsers.add_parser(
|
||||
"profile",
|
||||
help="Manage profiles — multiple isolated Hermes instances",
|
||||
)
|
||||
profile_subparsers = profile_parser.add_subparsers(dest="profile_action")
|
||||
|
||||
profile_subparsers.add_parser("list", help="List all profiles")
|
||||
profile_use = profile_subparsers.add_parser(
|
||||
"use", help="Set sticky default profile"
|
||||
)
|
||||
profile_use.add_argument("profile_name", help="Profile name (or 'default')")
|
||||
|
||||
profile_create = profile_subparsers.add_parser(
|
||||
"create", help="Create a new profile"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"profile_name", help="Profile name (lowercase, alphanumeric)"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--clone",
|
||||
action="store_true",
|
||||
help="Copy config.yaml, .env, SOUL.md from active profile",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--clone-all",
|
||||
action="store_true",
|
||||
help="Full copy of active profile (all state)",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--clone-from",
|
||||
metavar="SOURCE",
|
||||
help="Source profile to clone from (default: active)",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--no-alias", action="store_true", help="Skip wrapper script creation"
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--no-skills",
|
||||
action="store_true",
|
||||
help="Create an empty profile with no bundled skills (opts out of `hermes update` skill sync)",
|
||||
)
|
||||
profile_create.add_argument(
|
||||
"--description",
|
||||
default=None,
|
||||
help="One- or two-sentence description of what this profile is good at. "
|
||||
"Used by the kanban decomposer to route tasks based on role instead "
|
||||
"of profile name alone. Skip and add later via `hermes profile describe`.",
|
||||
)
|
||||
|
||||
profile_delete = profile_subparsers.add_parser("delete", help="Delete a profile")
|
||||
profile_delete.add_argument("profile_name", help="Profile to delete")
|
||||
profile_delete.add_argument(
|
||||
"-y", "--yes", action="store_true", help="Skip confirmation prompt"
|
||||
)
|
||||
|
||||
profile_describe = profile_subparsers.add_parser(
|
||||
"describe",
|
||||
help="Read or set a profile's description (used by the kanban orchestrator)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"profile_name",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Profile to describe (omit + use --all --auto to sweep)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--text",
|
||||
default=None,
|
||||
help="Set description to this exact text (overwrites any existing description)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--auto",
|
||||
action="store_true",
|
||||
help="Auto-generate description via the auxiliary LLM "
|
||||
"(uses auxiliary.profile_describer)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="With --auto, replace user-authored descriptions too (default: only "
|
||||
"fill in missing or previously-auto descriptions)",
|
||||
)
|
||||
profile_describe.add_argument(
|
||||
"--all",
|
||||
dest="all_missing",
|
||||
action="store_true",
|
||||
help="With --auto, run on every profile missing a description",
|
||||
)
|
||||
|
||||
profile_show = profile_subparsers.add_parser("show", help="Show profile details")
|
||||
profile_show.add_argument("profile_name", help="Profile to show")
|
||||
|
||||
profile_alias = profile_subparsers.add_parser(
|
||||
"alias", help="Manage wrapper scripts"
|
||||
)
|
||||
profile_alias.add_argument("profile_name", help="Profile name")
|
||||
profile_alias.add_argument(
|
||||
"--remove", action="store_true", help="Remove the wrapper script"
|
||||
)
|
||||
profile_alias.add_argument(
|
||||
"--name",
|
||||
dest="alias_name",
|
||||
metavar="NAME",
|
||||
help="Custom alias name (default: profile name)",
|
||||
)
|
||||
|
||||
profile_rename = profile_subparsers.add_parser("rename", help="Rename a profile")
|
||||
profile_rename.add_argument("old_name", help="Current profile name")
|
||||
profile_rename.add_argument("new_name", help="New profile name")
|
||||
|
||||
profile_export = profile_subparsers.add_parser(
|
||||
"export", help="Export a profile to archive"
|
||||
)
|
||||
profile_export.add_argument("profile_name", help="Profile to export")
|
||||
profile_export.add_argument(
|
||||
"-o", "--output", default=None, help="Output file (default: <name>.tar.gz)"
|
||||
)
|
||||
|
||||
profile_import = profile_subparsers.add_parser(
|
||||
"import", help="Import a profile from archive"
|
||||
)
|
||||
profile_import.add_argument("archive", help="Path to .tar.gz archive")
|
||||
profile_import.add_argument(
|
||||
"--name",
|
||||
dest="import_name",
|
||||
metavar="NAME",
|
||||
help="Profile name (default: inferred from archive)",
|
||||
)
|
||||
|
||||
# ---------- Distribution subcommands (issue #20456) ----------
|
||||
profile_install = profile_subparsers.add_parser(
|
||||
"install",
|
||||
help="Install a profile distribution from a git URL or local directory",
|
||||
description=(
|
||||
"Install a Hermes profile distribution. SOURCE can be a git URL "
|
||||
"(github.com/user/repo, https://..., git@...) or a local "
|
||||
"directory containing distribution.yaml at its root."
|
||||
),
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"source",
|
||||
help="Distribution source (git URL or local directory)",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"--name", dest="install_name", metavar="NAME",
|
||||
help="Override profile name (default: read from manifest)",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"--alias", action="store_true",
|
||||
help="Create a shell wrapper alias for the installed profile",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"--force", action="store_true",
|
||||
help="Overwrite an existing profile of the same name (user data preserved)",
|
||||
)
|
||||
profile_install.add_argument(
|
||||
"-y", "--yes", action="store_true",
|
||||
help="Skip manifest preview confirmation",
|
||||
)
|
||||
|
||||
profile_update = profile_subparsers.add_parser(
|
||||
"update",
|
||||
help="Re-pull a distribution and apply updates (user data preserved)",
|
||||
description=(
|
||||
"Fetch the distribution from its recorded source and overwrite "
|
||||
"distribution-owned files (SOUL.md, skills/, cron/, mcp.json). "
|
||||
"User data (memories, sessions, auth, .env) is never touched. "
|
||||
"config.yaml is preserved unless --force-config is passed."
|
||||
),
|
||||
)
|
||||
profile_update.add_argument("profile_name", help="Profile to update")
|
||||
profile_update.add_argument(
|
||||
"--force-config", action="store_true",
|
||||
help="Also overwrite config.yaml (normally preserved to keep user overrides)",
|
||||
)
|
||||
profile_update.add_argument(
|
||||
"-y", "--yes", action="store_true",
|
||||
help="Skip confirmation",
|
||||
)
|
||||
|
||||
profile_info = profile_subparsers.add_parser(
|
||||
"info",
|
||||
help="Show a profile's distribution manifest (version, requirements, source)",
|
||||
)
|
||||
profile_info.add_argument("profile_name", help="Profile to inspect")
|
||||
|
||||
profile_parser.set_defaults(func=cmd_profile)
|
||||
83
tests/hermes_cli/test_subcommands_profile_gateway.py
Normal file
83
tests/hermes_cli/test_subcommands_profile_gateway.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"""Unit tests for extracted subcommand parser builders (profile, gateway).
|
||||
|
||||
Confirms the builders attach the same subactions and ``func=`` dispatch that
|
||||
lived inline in ``main()`` before the god-file Phase 2 extraction.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
from hermes_cli.subcommands.gateway import build_gateway_parser
|
||||
from hermes_cli.subcommands.profile import build_profile_parser
|
||||
|
||||
|
||||
def _h_gateway(args): # pragma: no cover - identity only
|
||||
return "gateway"
|
||||
|
||||
|
||||
def _h_proxy(args): # pragma: no cover - identity only
|
||||
return "proxy"
|
||||
|
||||
|
||||
def _h_profile(args): # pragma: no cover - identity only
|
||||
return "profile"
|
||||
|
||||
|
||||
def _profile_parser():
|
||||
p = argparse.ArgumentParser(prog="hermes")
|
||||
sub = p.add_subparsers(dest="command")
|
||||
build_profile_parser(sub, cmd_profile=_h_profile)
|
||||
return p
|
||||
|
||||
|
||||
def _gateway_parser():
|
||||
p = argparse.ArgumentParser(prog="hermes")
|
||||
sub = p.add_subparsers(dest="command")
|
||||
build_gateway_parser(sub, cmd_gateway=_h_gateway, cmd_proxy=_h_proxy)
|
||||
return p
|
||||
|
||||
|
||||
def test_profile_subactions_and_dispatch():
|
||||
p = _profile_parser()
|
||||
ns = p.parse_args(["profile", "list"])
|
||||
assert ns.command == "profile"
|
||||
assert ns.profile_action == "list"
|
||||
assert ns.func is _h_profile
|
||||
# a representative arg-taking subaction
|
||||
ns2 = p.parse_args(["profile", "show", "work"])
|
||||
assert ns2.profile_action == "show"
|
||||
|
||||
|
||||
def test_profile_has_expected_actions():
|
||||
p = _profile_parser()
|
||||
# Map each subaction to a minimal valid argv suffix.
|
||||
cases = {
|
||||
"list": [],
|
||||
"use": ["work"],
|
||||
"create": ["work"],
|
||||
"delete": ["work"],
|
||||
"show": ["work"],
|
||||
"rename": ["old", "new"],
|
||||
"export": ["work"],
|
||||
"import": ["/tmp/x.zip"],
|
||||
}
|
||||
for action, extra in cases.items():
|
||||
ns = p.parse_args(["profile", action, *extra])
|
||||
assert ns.profile_action == action
|
||||
|
||||
|
||||
def test_gateway_and_proxy_dispatch():
|
||||
p = _gateway_parser()
|
||||
gw = p.parse_args(["gateway", "run"])
|
||||
assert gw.command == "gateway"
|
||||
assert gw.func is _h_gateway
|
||||
px = p.parse_args(["proxy"])
|
||||
assert px.command == "proxy"
|
||||
assert px.func is _h_proxy
|
||||
|
||||
|
||||
def test_gateway_accept_hooks_flag():
|
||||
p = _gateway_parser()
|
||||
ns = p.parse_args(["gateway", "run", "--accept-hooks"])
|
||||
assert ns.accept_hooks is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue