hermes_cli — Command-Line Interface

Command-Line Interface

The unified command-line interface and terminal UI for Hermes Agent — orchestrating interactive chat sessions, configuration, authentication, model selection, gateway management, tool discovery, multi-profile support, setup wizards, and a skin/theming engine across 50 files and ~25,000 lines of Python.

🔵 150+ Commands 🟢 100+ Providers 🟡 Multi-Profile 🔴 Setup Wizard

🎯 Why This Module Exists

Without hermes_cli, every tool that wanted to interact with an Hermes agent would need to implement its own argument parsing, authentication handshake, provider discovery, configuration loading, and terminal rendering logic. The CLI module exists to collapse all of those responsibilities into a single, cohesive entry point that speaks the language of the terminal user. It absorbs the complexity of 100+ model providers, multiple authentication strategies (OAuth device-code flows, API-key vaults, rotating credential pools), YAML-based multi-profile configuration, and a curses-driven interactive UI — so that the agent core can remain a pure reasoning engine free from I/O concerns. It also serves as the only place where managed-mode detection occurs, meaning enterprise deployments that inject corporate policies only need to hook into this one module. If hermes_cli were removed, every downstream consumer — scripts, CI runners, IDE extensions — would have to re-derive these capabilities independently, creating a combinatorial explosion of configuration bugs and auth failures.

💡 Real-world analogy

Think of hermes_cli as the cockpit of an airliner. The engines (agent core), navigation systems (providers), and fuel management (auth/credentials) are all complex subsystems, but the pilot interacts with them through a unified instrument panel with standardized gauges, switches, and checklists. The cockpit doesn’t fly the plane — it translates human intent into system commands, handles pre-flight checks (setup wizard), manages multiple flight profiles (multi-profile), and provides clear warnings when something goes wrong (doctor diagnostics). Without it, you’d be hot-wiring avionics by hand.

50
Files
150+
Commands
~25K
Lines
Python
Language

Architecture

flowchart LR
  subgraph Entry["Entry & Parsing"]
    MAIN["main.py\nargparse entry"]
    CMDS["commands.py\nCommandDef registry"]
  end
  subgraph Config["Configuration"]
    CFG["config.py\nYAML loader"]
    PROF["profiles.py\nMulti-profile"]
    SETUP["setup.py\nSetup wizard"]
    BACKUP["backup.py\nBackup/restore"]
  end
  subgraph Auth["Auth & Providers"]
    AUTH["auth.py\nOAuth / API keys"]
    PROV["providers.py\nProvider metadata"]
    RPROV["runtime_provider.py\nRuntime resolution"]
  end
  subgraph Models["Model Layer"]
    MOD["models.py\nModel registry"]
    MSWITCH["model_switch.py\nHot-swap"]
    MNORM["model_normalize.py\nName normalization"]
  end
  subgraph UI["Terminal UI"]
    CURSES["curses_ui.py\nMulti-select"]
    SKIN["skin_engine.py\nTheming"]
    CB["callbacks.py\nEvent callbacks"]
  end
  subgraph Services["Gateway & Services"]
    GW["gateway.py\nLifecycle CLI"]
    SKILLS["skills_hub.py\nSkills discovery"]
    DOCTOR["doctor.py\nDiagnostics"]
  end
  MAIN --> CMDS
  MAIN --> CFG
  CFG --> PROF
  CFG --> SETUP
  CMDS --> AUTH
  CMDS --> PROV
  PROV --> RPROV
  RPROV --> MOD
  MOD --> MSWITCH
  MOD --> MNORM
  CMDS --> CURSES
  CURSES --> SKIN
  CMDS --> GW
  CMDS --> SKILLS
  CMDS --> DOCTOR
  AUTH --> CB
  CFG --> BACKUP

Data Flow

flowchart TD
  USER["👤 User types\n'hermes chat -m gpt-4'"] --> ARGPARSE["Argparse\nparse_args()"]
  ARGPARSE --> PROFILE["Profile Pre-Parse\nload active profile"]
  PROFILE --> CONFIG["Config Loader\nYAML merge + managed-mode"]
  CONFIG --> AUTHFLOW["Auth Resolution\nOAuth / API key / pool"]
  AUTHFLOW --> PROVIDER["Provider Resolution\npriority chain lookup"]
  PROVIDER --> MODEL["Model Normalization\ncanonical name + routing"]
  MODEL --> AGENT["Agent Core\nreasoning engine"]
  AGENT --> CALLBACKS["Callbacks\nstreaming events"]
  CALLBACKS --> DISPLAY["Terminal Display\nskin engine + curses"]
  DISPLAY --> USER

🚶 Step-by-Step Narrated Walkthrough

Follow a single hermes chat -m gpt-4 command as it flows through the entire hermes_cli pipeline:

  1. Shell invocation — The user runs hermes chat -m gpt-4. Python’s __main__ hook calls main.py:main().
  2. Profile pre-parse — Before full argument parsing, profiles.py scans sys.argv for --profile to determine which config directory to load. This two-pass design lets profile selection happen before any config-dependent defaults are resolved.
  3. Argparse dispatch — main.py builds the full ArgumentParser with 150+ sub-commands registered from commands.py:CommandDef instances. The chat sub-command is matched and its handler cmd_chat() is selected.
  4. Config loading — config.py reads ~/.hermes/config.yaml (or the profile-specific path), merges environment overrides, and performs managed-mode detection by checking for a .hermes-managed sentinel file. Managed mode restricts which providers and settings are available.
  5. Auth resolution — auth.py determines the authentication strategy for the requested provider. For OpenAI, it checks (in order): environment variable OPENAI_API_KEY, credential pool file with file-locking, stored OAuth token, and finally falls back to an interactive OAuth device-code flow that prints a URL and waits for the user to authorize.
  6. Provider resolution — providers.py looks up the provider metadata for gpt-4, while runtime_provider.py applies the runtime priority chain: explicit -m flag > profile default > config default > environment variable > built-in fallback.
  7. Model normalization — model_normalize.py canonicalizes gpt-4 into its full API identifier (e.g., gpt-4-0613), resolving aliases and version shortcuts. model_switch.py prepares the hot-swap machinery so the user can change models mid-session with /model.
  8. Agent initialization — The CLI constructs the agent core with the resolved provider, model, and credential, then attaches callbacks.py event handlers for streaming token display, tool-call rendering, and error surfacing.
  9. Interactive loop — curses_ui.py (with graceful fallback to plain input() on dumb terminals) presents the prompt. The skin_engine.py applies the active theme’s color palette to all output elements.
  10. Slash-command dispatch — If the user types /skills, the input is intercepted by the command registry before reaching the agent. skills_hub.py queries the Skills Hub API, presents results via curses_ui.py multi-select, and installs chosen skills.
  11. Session teardown — On /exit or Ctrl-C, the CLI gracefully shuts down the agent, flushes any pending gateway connections (gateway.py), and writes session history to disk.

🏗️ Design Decisions

DecisionChoice MadeAlternatives ConsideredWhy This Approach
Command registry pattern CommandDef dataclass instances registered in a flat list Decorator-based auto-discovery; class-based command hierarchy A flat list of dataclasses makes commands trivially introspectable at startup (for help generation, fuzzy matching, and managed-mode filtering) without import-time side effects. Decorators would scatter registration across dozens of files, making it hard to enumerate all commands without importing everything.
Profile pre-parse (two-pass argv) Scan sys.argv for --profile before building the full parser Single-pass with lazy config; config path as env var only The full argparse tree depends on config values (e.g., default model, available providers), so the config directory must be known before argument defaults are resolved. A two-pass approach keeps the user-facing API clean (--profile works like any other flag) while avoiding circular dependencies.
Auth store file locking fcntl.flock() on the credential pool JSON file SQLite for credentials; in-memory only; OS keychain File locking is zero-dependency and works across all Unix platforms. SQLite adds a dependency and complicates atomic reads for a file that changes rarely. OS keychains are platform-specific and don’t support the pool-rotation pattern (multiple valid keys for the same provider, round-robined to distribute rate limits).
Managed-mode detection Sentinel file .hermes-managed in the config directory Config key in YAML; environment variable; server-side flag A sentinel file can be dropped by an MDM or provisioning script without modifying the user’s config. It’s visible in ls, easy to debug, and survives config resets. Environment variables are invisible and easy to override; a YAML key could be accidentally deleted by the user.
Curses graceful degradation Try curses.wrapper(), catch curses.error, fall back to plain input() Always use plain I/O; require curses; use a TUI framework like Textual Many CI environments and minimal Docker containers lack a proper terminal. Graceful fallback means the same binary works everywhere — rich UI when available, functional CLI when not — without requiring users to install a TUI framework or configure TERM.
Skin engine as data-driven config JSON/YAML theme files mapping semantic tokens to ANSI codes Hardcoded ANSI strings; CSS-like stylesheet; plugin-based themes Data-driven themes can be shipped, shared, and overridden without code changes. Users can create custom skins by copying a JSON file, and managed-mode deployments can enforce a corporate theme by dropping a skin file into the config directory.

🔬 Algorithm Spotlight

Provider Resolution Priority Chain

The Problem: When the user requests a model like gpt-4, multiple configuration sources may specify different providers and credentials. The CLI must deterministically resolve which provider endpoint and API key to use, respecting explicit overrides while providing sensible defaults.

The Approach: The resolution follows a strict priority chain evaluated top-to-bottom. At each level, if a value is found, it is used and lower levels are skipped. The chain is: (1) explicit CLI flag (-m/--provider), (2) active profile’s default, (3) global config default, (4) environment variable (HERMES_PROVIDER), (5) built-in fallback. Each level can specify both the provider and the model independently, so a user can override the model while inheriting the profile’s provider.

Why This Algorithm: A simple “last-write-wins” merge would create unpredictable behavior when environment variables and config files interact. The explicit priority chain makes the outcome deterministic and debuggable — users can run hermes doctor to see exactly which level each setting was resolved from. The time complexity is O(1) per setting (constant number of levels), so it adds zero overhead to startup.

graph TD
    INPUT["Model request\n'gpt-4'"] --> CLI_FLAG{"-m flag\nprovided?"}
    CLI_FLAG -->|Yes| USE_FLAG["Use CLI flag\nprovider + model"]
    CLI_FLAG -->|No| PROFILE{Profile\ndefault set?}
    PROFILE -->|Yes| USE_PROFILE["Use profile\ndefault"]
    PROFILE -->|No| CONFIG{Global config\ndefault set?}
    CONFIG -->|Yes| USE_CONFIG["Use config\ndefault"]
    CONFIG -->|No| ENV{ENV var\nHERMES_PROVIDER?}
    ENV -->|Yes| USE_ENV["Use env var"]
    ENV -->|No| FALLBACK["Built-in\nfallback provider"]
    USE_FLAG --> NORMALIZE["model_normalize.py\nCanonicalize name"]
    USE_PROFILE --> NORMALIZE
    USE_CONFIG --> NORMALIZE
    USE_ENV --> NORMALIZE
    FALLBACK --> NORMALIZE
    NORMALIZE --> RESOLVE["runtime_provider.py\nBuild API client"]
Trace through an example

Given: User runs hermes chat -m claude-3 with profile “work” that defaults to gpt-4 and a global config that defaults to gpt-3.5-turbo.

Step 1: Check CLI flag — -m claude-3 is present. Use it immediately.

Step 2: Skip profile default (gpt-4) and global default (gpt-3.5-turbo) — CLI flag has highest priority.

Step 3: model_normalize.py resolves claude-3 to claude-3-sonnet-20240229 and determines the provider is Anthropic.

Step 4: auth.py looks up Anthropic credentials in the priority chain (env var ANTHROPIC_API_KEY > credential pool > OAuth).

Result: The session uses claude-3-sonnet-20240229 via Anthropic, regardless of profile or config defaults. If the user had omitted -m, the “work” profile’s gpt-4 default would have been used instead.

⚠️ What Could Go Wrong

Failure ModeTriggerDetectionRecovery / Mitigation
OAuth device-code timeout User doesn’t complete browser authorization within the provider’s TTL (typically 5–15 minutes) auth.py polls the token endpoint and receives an expired_token error The CLI prints a clear message explaining the timeout, offers to restart the flow, and suggests using an API key instead if repeated failures occur
Credential pool exhaustion All API keys in the pool have hit their rate limits simultaneously auth.py tracks per-key 429 responses and detects when the rotation cycle has no remaining keys Backs off with exponential retry, logs which keys are exhausted and their reset times, and switches to a fallback provider if configured in the profile
Curses initialization failure Running in a non-interactive terminal (CI, Docker without TTY, piped input) curses_ui.py catches curses.error and _curses.error during wrapper() initialization Silently falls back to plain input()/print() I/O. All features remain functional; only multi-select pickers and colored output are lost
Config file corruption Interrupted write during config save, manual editing errors, or encoding issues config.py catches yaml.YAMLError and validates the parsed structure against a schema backup.py maintains rotating backups of the config. On corruption, the CLI offers to restore from the most recent valid backup or re-run the setup wizard
Gateway connection loss Network interruption, gateway process crash, or port conflict gateway.py monitors the health-check endpoint and detects socket errors Attempts reconnection with backoff. If the gateway process crashed, offers to restart it. Queued requests are retried after reconnection

⚡ Performance Profile

Hot Paths

The most performance-sensitive code paths in this module:

Resource Usage

Key Components

FileLinesRoleKey Responsibilities
main.py~4500Entry pointArgparse setup, cmd_* handler dispatch, profile pre-parse, startup orchestration
commands.py~800Command registryCommandDef dataclass definitions for 150+ slash commands, help text, permission flags
config.py~600Config loaderYAML parsing, env-var override merge, managed-mode sentinel detection, schema validation
auth.py~3200AuthenticationOAuth device-code flow, API key management, credential pool rotation, file locking, token refresh
setup.py~700Setup wizardInteractive first-run configuration, provider selection, API key entry, config file generation
models.py~1200Model registryKnown-model catalog, capability metadata, pricing info, context-window limits
model_switch.py~400Hot-swapMid-session model switching via /model command, session state migration
model_normalize.py~350Name normalizationAlias resolution, version suffix handling, canonical API identifier mapping
providers.py~900Provider metadataStatic registry of provider endpoints, auth methods, rate limits, feature flags
runtime_provider.py~500Runtime resolutionPriority chain evaluation, API client construction, endpoint URL assembly
curses_ui.py~600Terminal UICurses multi-select picker, scrolling list, graceful fallback to plain I/O
skin_engine.py~450ThemingData-driven ANSI color themes, semantic token mapping, custom skin loading
skills_hub.py~500Skills discoverySkills Hub API client, search/install/update workflows, dependency resolution
gateway.py~550Gateway CLIGateway process start/stop/restart, health monitoring, port management
doctor.py~400DiagnosticsSystem checks, config validation, auth verification, provider connectivity tests

Sub-Modules

🛠️ Core (Entry & Config)

The foundational trio that bootstraps every CLI session — parsing arguments, loading configuration, and dispatching commands.

  • main.py — argparse entry, cmd_* handlers
  • commands.py — CommandDef registry
  • config.py — YAML config loader
  • profiles.py — Multi-profile isolation
  • setup.py — Interactive setup wizard
  • backup.py — Config backup/restore

🔒 Auth & Providers

Handles all credential management and provider endpoint resolution across 100+ services.

  • auth.py — OAuth, API keys, credential pools
  • providers.py — Provider metadata registry
  • runtime_provider.py — Runtime priority resolution

🤖 Models

Model catalog, alias resolution, and mid-session model hot-swapping.

  • models.py — Known-model catalog
  • model_switch.py — Hot-swap machinery
  • model_normalize.py — Name normalization

🎨 UI/UX

Terminal rendering, interactive pickers, theming, and event-driven output.

  • curses_ui.py — Curses multi-select
  • skin_engine.py — Data-driven theming
  • callbacks.py — Agent event callbacks

🌐 Gateway & Services

External service integration — gateway lifecycle, skills marketplace, and diagnostic tooling.

  • gateway.py — Gateway lifecycle CLI
  • skills_hub.py — Skills Hub integration
  • skills_config.py — Skills configuration
  • doctor.py — System diagnostics

Directory Map

Source tree for hermes_cli
hermes_cli/
├── main.py                    ← Entry point: argparse, cmd_* dispatch (~4500 LOC)
├── commands.py                ← CommandDef registry for 150+ slash commands
├── config.py                  ← YAML config loader, managed-mode detection
├── profiles.py                ← Multi-profile isolation and switching
├── setup.py                   ← Interactive first-run setup wizard
├── backup.py                  ← Config backup and restore utilities
├── auth.py                    ← Multi-provider auth, OAuth, credential pools (~3200 LOC)
├── providers.py               ← Static provider metadata registry
├── runtime_provider.py        ← Runtime provider priority chain resolution
├── models.py                  ← Known-model catalog with capabilities/pricing
├── model_switch.py            ← Mid-session model hot-swap
├── model_normalize.py         ← Model name alias resolution
├── curses_ui.py               ← Curses multi-select with graceful fallback
├── skin_engine.py             ← Data-driven ANSI theming engine
├── callbacks.py               ← Agent event callback handlers
├── gateway.py                 ← Gateway process lifecycle CLI
├── skills_hub.py              ← Skills Hub API client and workflows
├── skills_config.py           ← Skills configuration management
├── doctor.py                  ← Diagnostic checks and system validation
├── constants.py               ← Shared constants and defaults
├── exceptions.py              ← Module-specific exception hierarchy
├── utils.py                   ← Shared utility functions
├── types.py                   ← Type definitions and protocols
└── __init__.py                ← Package init, version export