refactor(hermes_cli): split cmd_chat prologue into helpers; split build_top_level_parser into flags + chat builders
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user