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.
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:
- Shell invocation — The user runs
hermes chat -m gpt-4. Python’s__main__hook callsmain.py:main(). - Profile pre-parse — Before full argument parsing,
profiles.pyscanssys.argvfor--profileto determine which config directory to load. This two-pass design lets profile selection happen before any config-dependent defaults are resolved. - Argparse dispatch —
main.pybuilds the fullArgumentParserwith 150+ sub-commands registered fromcommands.py:CommandDefinstances. Thechatsub-command is matched and its handlercmd_chat()is selected. - Config loading —
config.pyreads~/.hermes/config.yaml(or the profile-specific path), merges environment overrides, and performs managed-mode detection by checking for a.hermes-managedsentinel file. Managed mode restricts which providers and settings are available. - Auth resolution —
auth.pydetermines the authentication strategy for the requested provider. For OpenAI, it checks (in order): environment variableOPENAI_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. - Provider resolution —
providers.pylooks up the provider metadata forgpt-4, whileruntime_provider.pyapplies the runtime priority chain: explicit-mflag > profile default > config default > environment variable > built-in fallback. - Model normalization —
model_normalize.pycanonicalizesgpt-4into its full API identifier (e.g.,gpt-4-0613), resolving aliases and version shortcuts.model_switch.pyprepares the hot-swap machinery so the user can change models mid-session with/model. - Agent initialization — The CLI constructs the agent core with the resolved provider, model, and credential, then attaches
callbacks.pyevent handlers for streaming token display, tool-call rendering, and error surfacing. - Interactive loop —
curses_ui.py(with graceful fallback to plaininput()on dumb terminals) presents the prompt. Theskin_engine.pyapplies the active theme’s color palette to all output elements. - Slash-command dispatch — If the user types
/skills, the input is intercepted by the command registry before reaching the agent.skills_hub.pyqueries the Skills Hub API, presents results viacurses_ui.pymulti-select, and installs chosen skills. - Session teardown — On
/exitor Ctrl-C, the CLI gracefully shuts down the agent, flushes any pending gateway connections (gateway.py), and writes session history to disk.
🏗️ Design Decisions
| Decision | Choice Made | Alternatives Considered | Why 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 Mode | Trigger | Detection | Recovery / 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:
- Startup to first prompt: Profile pre-parse + config load + auth check — target <500ms. The two-pass argv scan is O(n) on argument count (negligible). Config loading is I/O-bound on a single YAML file (~1ms). Auth is the bottleneck only when an OAuth flow is triggered (user-interactive, not counted).
- Command dispatch: Slash-command lookup in the
CommandDefregistry — O(n) linear scan over ~150 entries with short-circuit on prefix match. At 150 entries this is sub-microsecond; a hash map would save nothing and lose fuzzy-match capability. - Streaming token rendering:
callbacks.pyreceives tokens one at a time and writes to stdout. The skin engine applies ANSI codes per-token — O(1) per token. Buffering is left to the terminal emulator.
Resource Usage
- Memory: The command registry, provider metadata, and model catalog are loaded once at startup (~2MB). Conversation history is held in memory for the session duration; long sessions with tool calls can grow to tens of MB. No explicit caching layer — the agent core owns response caching.
- I/O: Config files are read once at startup and written only on explicit save. The credential pool file is opened with
flock()for each auth check (~1 read per command). Gateway health checks are periodic (every 30s) and use a single HTTP GET. - Network: Provider API calls are pass-through to the agent core. The CLI itself makes network calls only for OAuth flows (token endpoint polling) and Skills Hub queries (on-demand).
Key Components
| File | Lines | Role | Key Responsibilities |
|---|---|---|---|
main.py | ~4500 | Entry point | Argparse setup, cmd_* handler dispatch, profile pre-parse, startup orchestration |
commands.py | ~800 | Command registry | CommandDef dataclass definitions for 150+ slash commands, help text, permission flags |
config.py | ~600 | Config loader | YAML parsing, env-var override merge, managed-mode sentinel detection, schema validation |
auth.py | ~3200 | Authentication | OAuth device-code flow, API key management, credential pool rotation, file locking, token refresh |
setup.py | ~700 | Setup wizard | Interactive first-run configuration, provider selection, API key entry, config file generation |
models.py | ~1200 | Model registry | Known-model catalog, capability metadata, pricing info, context-window limits |
model_switch.py | ~400 | Hot-swap | Mid-session model switching via /model command, session state migration |
model_normalize.py | ~350 | Name normalization | Alias resolution, version suffix handling, canonical API identifier mapping |
providers.py | ~900 | Provider metadata | Static registry of provider endpoints, auth methods, rate limits, feature flags |
runtime_provider.py | ~500 | Runtime resolution | Priority chain evaluation, API client construction, endpoint URL assembly |
curses_ui.py | ~600 | Terminal UI | Curses multi-select picker, scrolling list, graceful fallback to plain I/O |
skin_engine.py | ~450 | Theming | Data-driven ANSI color themes, semantic token mapping, custom skin loading |
skills_hub.py | ~500 | Skills discovery | Skills Hub API client, search/install/update workflows, dependency resolution |
gateway.py | ~550 | Gateway CLI | Gateway process start/stop/restart, health monitoring, port management |
doctor.py | ~400 | Diagnostics | System 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_* handlerscommands.py— CommandDef registryconfig.py— YAML config loaderprofiles.py— Multi-profile isolationsetup.py— Interactive setup wizardbackup.py— Config backup/restore
🔒 Auth & Providers
Handles all credential management and provider endpoint resolution across 100+ services.
auth.py— OAuth, API keys, credential poolsproviders.py— Provider metadata registryruntime_provider.py— Runtime priority resolution
🤖 Models
Model catalog, alias resolution, and mid-session model hot-swapping.
models.py— Known-model catalogmodel_switch.py— Hot-swap machinerymodel_normalize.py— Name normalization
🎨 UI/UX
Terminal rendering, interactive pickers, theming, and event-driven output.
curses_ui.py— Curses multi-selectskin_engine.py— Data-driven themingcallbacks.py— Agent event callbacks
🌐 Gateway & Services
External service integration — gateway lifecycle, skills marketplace, and diagnostic tooling.
gateway.py— Gateway lifecycle CLIskills_hub.py— Skills Hub integrationskills_config.py— Skills configurationdoctor.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