refactor(hermes_cli): split cmd_chat prologue into helpers; split build_top_level_parser into flags + chat builders

This commit is contained in:
Teknium
2026-09-02 16:33:36 -07:00
parent ffa08e6591
commit 6eae28cb6d
2 changed files with 94 additions and 58 deletions

View File

@@ -1,8 +1,8 @@
"""Top-level argparse construction for the hermes CLI.
Only the top-level parser and the ``chat`` subparser live here. Every other subparser (model,
gateway, sessions, …) is built inline in ``main.py`` because its dispatch is tightly coupled to
module-level ``cmd_*`` functions.
gateway, sessions, …) is built by ``hermes_cli/subcommands/<group>.py`` and wired in
``main._build_cli_parser`` with its ``cmd_*`` handler injected.
"""
import argparse
@@ -120,20 +120,8 @@ For more help on a command:
"""
def build_top_level_parser():
"""Build the top-level parser, the subparsers action, and the ``chat`` subparser.
Returns ``(parser, subparsers, chat_parser)``; the caller wires
``chat_parser.set_defaults(func= cmd_chat)`` and registers further subparsers via
``subparsers.add_parser(...)``.
"""
parser = argparse.ArgumentParser(
prog="hermes",
description="Hermes Agent - AI assistant with tool-calling capabilities",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=_EPILOGUE,
)
def _add_top_level_flags(parser: argparse.ArgumentParser) -> None:
"""Top-level (pre-subcommand) flags; ``-m/--provider`` pair with ``-z`` without ``chat``."""
parser.add_argument(
"--version", "-V", action="store_true", help="Show version and exit"
)
@@ -326,11 +314,9 @@ def build_top_level_parser():
help="With --tui: run TypeScript sources via tsx (skip dist build)",
)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
# =========================================================================
# chat command
# =========================================================================
def _build_chat_parser(subparsers) -> argparse.ArgumentParser:
"""The ``chat`` subparser (also the implicit default command)."""
chat_parser = subparsers.add_parser(
"chat",
help="Interactive chat with the agent",
@@ -592,5 +578,26 @@ def build_top_level_parser():
default=argparse.SUPPRESS,
help="With --tui: run TypeScript sources via tsx (skip dist build)",
)
return chat_parser
def build_top_level_parser():
"""Build the top-level parser, the subparsers action, and the ``chat`` subparser.
Returns ``(parser, subparsers, chat_parser)``; the caller wires
``chat_parser.set_defaults(func= cmd_chat)`` and registers further subparsers via
``subparsers.add_parser(...)``.
"""
parser = argparse.ArgumentParser(
prog="hermes",
description="Hermes Agent - AI assistant with tool-calling capabilities",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=_EPILOGUE,
)
_add_top_level_flags(parser)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
chat_parser = _build_chat_parser(subparsers)
return parser, subparsers, chat_parser

View File

@@ -416,7 +416,6 @@ if _try_ultrafast_version():
import argparse
import json
import re
import shlex
import shutil
import subprocess
from pathlib import Path
@@ -1803,13 +1802,16 @@ def _resolve_continue_arg(args, *, use_tui: bool) -> None:
sys.exit(1)
def cmd_chat(args):
"""Run interactive chat CLI."""
_apply_safe_mode(args)
_apply_user_config_bypass(args)
_guard_noninteractive_user_config(args)
use_tui = _resolve_use_tui(args)
def _resolve_chat_session_args(args, use_tui: bool) -> None:
"""Normalize --in / --resume / --continue on ``args`` before agent init.
Order matters: ``--in DIR`` chdirs first so workspace-scoped "latest"/-c
lookups key off DIR (and pins the session there, skipping cwd restore);
then ``--resume latest`` → MRU id, ``--continue`` → ``--resume``,
``--resume @claude/@codex`` → imported session id, title → id; finally
cd back into a resumed session's recorded cwd (best-effort, opt-out via
--no-restore-cwd, skipped under --worktree).
"""
# --in DIR: run in DIR. Must happen before any session resolution so the
# workspace-scoped "latest"/-c lookups key off DIR, and it pins the
# session there — an explicit --in wins over a resumed session's
@@ -1919,7 +1921,9 @@ def cmd_chat(args):
except Exception:
pass
# xAI retirement warning — one-shot, non-blocking, never fails startup
def _warn_retired_xai_models() -> None:
"""One-shot xAI retirement warning on stderr; non-blocking, never fails startup."""
try:
from hermes_cli.xai_retirement import (
MIGRATION_GUIDE_URL,
@@ -1942,38 +1946,17 @@ def cmd_chat(args):
except Exception:
pass
# First-run guard: check if any provider is configured before launching
if not _has_any_provider_configured():
print()
print(
"It looks like Hermes isn't configured yet -- no API keys or providers found."
)
print()
print(" Run: hermes setup")
print()
from hermes_cli.setup import (
is_interactive_stdin,
print_noninteractive_setup_guidance,
)
if not is_interactive_stdin():
print_noninteractive_setup_guidance(
"No interactive TTY detected for the first-run setup prompt."
)
sys.exit(1)
try:
reply = input("Run setup now? [Y/n] ").strip().lower()
except (EOFError, KeyboardInterrupt):
reply = "n"
if reply in {"", "y", "yes"}:
cmd_setup(args)
return
print()
print("You can run 'hermes setup' at any time to configure.")
sys.exit(1)
def _start_chat_background_prefetch() -> None:
"""Kick off the update-check/banner prefetch and the bundled-skills sync.
Update check is opt-in on Termux (it imports rich/prompt_toolkit in the
foreground and competes for CPU on single-core devices). The skills sync
is idempotent and hash-gated, so it normally runs in a daemon thread;
the ONE exception is an unseeded ~/.hermes/skills — there the banner
prefetch would race the sync and cache an empty skills index, so the
first run syncs in the foreground and drops the banner's skills cache.
"""
# Start update check in background (runs while other init happens).
# On Termux this imports rich/prompt_toolkit in the foreground and then
# competes for CPU on single-core devices, so keep it opt-in there.
@@ -2033,6 +2016,52 @@ def cmd_chat(args):
target=_skills_sync_bg, name="bundled-skills-sync", daemon=True
).start()
def cmd_chat(args):
"""Run interactive chat CLI."""
_apply_safe_mode(args)
_apply_user_config_bypass(args)
_guard_noninteractive_user_config(args)
use_tui = _resolve_use_tui(args)
_resolve_chat_session_args(args, use_tui)
_warn_retired_xai_models()
# First-run guard: check if any provider is configured before launching
if not _has_any_provider_configured():
print()
print(
"It looks like Hermes isn't configured yet -- no API keys or providers found."
)
print()
print(" Run: hermes setup")
print()
from hermes_cli.setup import (
is_interactive_stdin,
print_noninteractive_setup_guidance,
)
if not is_interactive_stdin():
print_noninteractive_setup_guidance(
"No interactive TTY detected for the first-run setup prompt."
)
sys.exit(1)
try:
reply = input("Run setup now? [Y/n] ").strip().lower()
except (EOFError, KeyboardInterrupt):
reply = "n"
if reply in {"", "y", "yes"}:
cmd_setup(args)
return
print()
print("You can run 'hermes setup' at any time to configure.")
sys.exit(1)
_start_chat_background_prefetch()
# --yolo: bypass all dangerous command approvals.
# Also set in main() before _prepare_agent_startup() — that is the
# authoritative site because it runs before tool imports freeze