2026-03-07 07:35:36 -08:00
|
|
|
"""Welcome banner, ASCII art, skills summary, and update check for the CLI.
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
Pure display functions with no HermesCLI state dependency.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-03-07 07:35:36 -08:00
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
2026-03-14 14:02:57 +07:00
|
|
|
import shutil
|
2026-03-07 07:35:36 -08:00
|
|
|
import subprocess
|
2026-03-14 14:02:57 +07:00
|
|
|
import threading
|
2026-03-07 07:35:36 -08:00
|
|
|
import time
|
2026-02-21 23:17:18 -08:00
|
|
|
from pathlib import Path
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
from hermes_constants import get_hermes_home
|
chore: remove ~100 unused imports across 55 files (#3016)
Automated cleanup via pyflakes + autoflake with manual review.
Changes:
- Removed unused stdlib imports (os, sys, json, pathlib.Path, etc.)
- Removed unused typing imports (List, Dict, Any, Optional, Tuple, Set, etc.)
- Removed unused internal imports (hermes_cli.auth, hermes_cli.config, etc.)
- Fixed cli.py: removed 8 shadowed banner imports (imported from hermes_cli.banner
then immediately redefined locally — only build_welcome_banner is actually used)
- Added noqa comments to imports that appear unused but serve a purpose:
- Re-exports (gateway/session.py SessionResetPolicy, tools/terminal_tool.py
is_interrupted/_interrupt_event)
- SDK presence checks in try/except (daytona, fal_client, discord)
- Test mock targets (auxiliary_client.py Path, mcp_config.py get_hermes_home)
Zero behavioral changes. Full test suite passes (6162/6162, 2 pre-existing
streaming test failures unrelated to this change).
2026-03-25 15:02:03 -07:00
|
|
|
from typing import Dict, List, Optional
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
from rich.console import Console
|
|
|
|
|
from rich.panel import Panel
|
|
|
|
|
from rich.table import Table
|
|
|
|
|
|
|
|
|
|
from prompt_toolkit import print_formatted_text as _pt_print
|
|
|
|
|
from prompt_toolkit.formatted_text import ANSI as _PT_ANSI
|
|
|
|
|
|
2026-03-07 07:35:36 -08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# ANSI building blocks for conversation display
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
2026-03-20 18:17:38 -07:00
|
|
|
_GOLD = "\033[1;38;2;255;215;0m" # True-color #FFD700 bold
|
2026-02-21 23:17:18 -08:00
|
|
|
_BOLD = "\033[1m"
|
|
|
|
|
_DIM = "\033[2m"
|
|
|
|
|
_RST = "\033[0m"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cprint(text: str):
|
|
|
|
|
"""Print ANSI-colored text through prompt_toolkit's renderer."""
|
|
|
|
|
_pt_print(_PT_ANSI(text))
|
|
|
|
|
|
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
# =========================================================================
|
|
|
|
|
# Skin-aware color helpers
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
|
|
|
|
def _skin_color(key: str, fallback: str) -> str:
|
|
|
|
|
"""Get a color from the active skin, or return fallback."""
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.skin_engine import get_active_skin
|
|
|
|
|
return get_active_skin().get_color(key, fallback)
|
|
|
|
|
except Exception:
|
|
|
|
|
return fallback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _skin_branding(key: str, fallback: str) -> str:
|
|
|
|
|
"""Get a branding string from the active skin, or return fallback."""
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.skin_engine import get_active_skin
|
|
|
|
|
return get_active_skin().get_branding(key, fallback)
|
|
|
|
|
except Exception:
|
|
|
|
|
return fallback
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 23:17:18 -08:00
|
|
|
# =========================================================================
|
|
|
|
|
# ASCII Art & Branding
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
2026-03-12 01:35:47 -07:00
|
|
|
from hermes_cli import __version__ as VERSION, __release_date__ as RELEASE_DATE
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
HERMES_AGENT_LOGO = """[bold #FFD700]██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
|
|
|
|
[bold #FFD700]██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
|
|
|
|
[#FFBF00]███████║█████╗ ██████╔╝██╔████╔██║█████╗ ███████╗█████╗███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║[/]
|
|
|
|
|
[#FFBF00]██╔══██║██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══╝ ╚════██║╚════╝██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║[/]
|
|
|
|
|
[#CD7F32]██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗███████║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║[/]
|
|
|
|
|
[#CD7F32]╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝[/]"""
|
|
|
|
|
|
|
|
|
|
HERMES_CADUCEUS = """[#CD7F32]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡀⠀⣀⣀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#CD7F32]⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⣿⣿⣇⠸⣿⣿⠇⣸⣿⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#FFBF00]⠀⢀⣠⣴⣶⠿⠋⣩⡿⣿⡿⠻⣿⡇⢠⡄⢸⣿⠟⢿⣿⢿⣍⠙⠿⣶⣦⣄⡀⠀[/]
|
|
|
|
|
[#FFBF00]⠀⠀⠉⠉⠁⠶⠟⠋⠀⠉⠀⢀⣈⣁⡈⢁⣈⣁⡀⠀⠉⠀⠙⠻⠶⠈⠉⠉⠀⠀[/]
|
|
|
|
|
[#FFD700]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⡿⠛⢁⡈⠛⢿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#FFD700]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠿⣿⣦⣤⣈⠁⢠⣴⣿⠿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#FFBF00]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠻⢿⣿⣦⡉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#FFBF00]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢷⣦⣈⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#CD7F32]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣴⠦⠈⠙⠿⣦⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#CD7F32]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣤⡈⠁⢤⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠷⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠑⢶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠁⢰⡆⠈⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⠈⣡⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
[#B8860B]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]"""
|
|
|
|
|
|
|
|
|
|
COMPACT_BANNER = """
|
|
|
|
|
[bold #FFD700]╔══════════════════════════════════════════════════════════════╗[/]
|
|
|
|
|
[bold #FFD700]║[/] [#FFBF00]⚕ NOUS HERMES[/] [dim #B8860B]- AI Agent Framework[/] [bold #FFD700]║[/]
|
|
|
|
|
[bold #FFD700]║[/] [#CD7F32]Messenger of the Digital Gods[/] [dim #B8860B]Nous Research[/] [bold #FFD700]║[/]
|
|
|
|
|
[bold #FFD700]╚══════════════════════════════════════════════════════════════╝[/]
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Skills scanning
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
|
|
|
|
def get_available_skills() -> Dict[str, List[str]]:
|
2026-03-18 03:17:37 -07:00
|
|
|
"""Return skills grouped by category, filtered by platform and disabled state.
|
2026-02-21 23:17:18 -08:00
|
|
|
|
2026-03-18 03:17:37 -07:00
|
|
|
Delegates to ``_find_all_skills()`` from ``tools/skills_tool`` which already
|
|
|
|
|
handles platform gating (``platforms:`` frontmatter) and respects the
|
|
|
|
|
user's ``skills.disabled`` config list.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from tools.skills_tool import _find_all_skills
|
|
|
|
|
all_skills = _find_all_skills() # already filtered
|
|
|
|
|
except Exception:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
skills_by_category: Dict[str, List[str]] = {}
|
|
|
|
|
for skill in all_skills:
|
|
|
|
|
category = skill.get("category") or "general"
|
|
|
|
|
skills_by_category.setdefault(category, []).append(skill["name"])
|
2026-02-21 23:17:18 -08:00
|
|
|
return skills_by_category
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 07:35:36 -08:00
|
|
|
# =========================================================================
|
|
|
|
|
# Update check
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
|
|
|
|
# Cache update check results for 6 hours to avoid repeated git fetches
|
|
|
|
|
_UPDATE_CHECK_CACHE_SECONDS = 6 * 3600
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_for_updates() -> Optional[int]:
|
|
|
|
|
"""Check how many commits behind origin/main the local repo is.
|
|
|
|
|
|
|
|
|
|
Does a ``git fetch`` at most once every 6 hours (cached to
|
|
|
|
|
``~/.hermes/.update_check``). Returns the number of commits behind,
|
|
|
|
|
or ``None`` if the check fails or isn't applicable.
|
|
|
|
|
"""
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
hermes_home = get_hermes_home()
|
2026-03-07 07:35:36 -08:00
|
|
|
repo_dir = hermes_home / "hermes-agent"
|
|
|
|
|
cache_file = hermes_home / ".update_check"
|
|
|
|
|
|
2026-03-14 14:02:57 +07:00
|
|
|
# Must be a git repo — fall back to project root for dev installs
|
|
|
|
|
if not (repo_dir / ".git").exists():
|
|
|
|
|
repo_dir = Path(__file__).parent.parent.resolve()
|
2026-03-07 07:35:36 -08:00
|
|
|
if not (repo_dir / ".git").exists():
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Read cache
|
|
|
|
|
now = time.time()
|
|
|
|
|
try:
|
|
|
|
|
if cache_file.exists():
|
|
|
|
|
cached = json.loads(cache_file.read_text())
|
|
|
|
|
if now - cached.get("ts", 0) < _UPDATE_CHECK_CACHE_SECONDS:
|
|
|
|
|
return cached.get("behind")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Fetch latest refs (fast — only downloads ref metadata, no files)
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["git", "fetch", "origin", "--quiet"],
|
|
|
|
|
capture_output=True, timeout=10,
|
|
|
|
|
cwd=str(repo_dir),
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # Offline or timeout — use stale refs, that's fine
|
|
|
|
|
|
|
|
|
|
# Count commits behind
|
|
|
|
|
try:
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
["git", "rev-list", "--count", "HEAD..origin/main"],
|
|
|
|
|
capture_output=True, text=True, timeout=5,
|
|
|
|
|
cwd=str(repo_dir),
|
|
|
|
|
)
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
behind = int(result.stdout.strip())
|
|
|
|
|
else:
|
|
|
|
|
behind = None
|
|
|
|
|
except Exception:
|
|
|
|
|
behind = None
|
|
|
|
|
|
|
|
|
|
# Write cache
|
|
|
|
|
try:
|
|
|
|
|
cache_file.write_text(json.dumps({"ts": now, "behind": behind}))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return behind
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 14:02:57 +07:00
|
|
|
# =========================================================================
|
|
|
|
|
# Non-blocking update check
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
|
|
|
|
_update_result: Optional[int] = None
|
|
|
|
|
_update_check_done = threading.Event()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prefetch_update_check():
|
|
|
|
|
"""Kick off update check in a background daemon thread."""
|
|
|
|
|
def _run():
|
|
|
|
|
global _update_result
|
|
|
|
|
_update_result = check_for_updates()
|
|
|
|
|
_update_check_done.set()
|
|
|
|
|
t = threading.Thread(target=_run, daemon=True)
|
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_update_result(timeout: float = 0.5) -> Optional[int]:
|
|
|
|
|
"""Get result of prefetched check. Returns None if not ready."""
|
|
|
|
|
_update_check_done.wait(timeout=timeout)
|
|
|
|
|
return _update_result
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 23:17:18 -08:00
|
|
|
# =========================================================================
|
|
|
|
|
# Welcome banner
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
2026-03-05 16:09:57 -08:00
|
|
|
def _format_context_length(tokens: int) -> str:
|
|
|
|
|
"""Format a token count for display (e.g. 128000 → '128K', 1048576 → '1M')."""
|
|
|
|
|
if tokens >= 1_000_000:
|
|
|
|
|
val = tokens / 1_000_000
|
|
|
|
|
return f"{val:g}M"
|
|
|
|
|
elif tokens >= 1_000:
|
|
|
|
|
val = tokens / 1_000
|
|
|
|
|
return f"{val:g}K"
|
|
|
|
|
return str(tokens)
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 03:22:58 -07:00
|
|
|
def _display_toolset_name(toolset_name: str) -> str:
|
|
|
|
|
"""Normalize internal/legacy toolset identifiers for banner display."""
|
|
|
|
|
if not toolset_name:
|
|
|
|
|
return "unknown"
|
|
|
|
|
return (
|
|
|
|
|
toolset_name[:-6]
|
|
|
|
|
if toolset_name.endswith("_tools")
|
|
|
|
|
else toolset_name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 23:17:18 -08:00
|
|
|
def build_welcome_banner(console: Console, model: str, cwd: str,
|
|
|
|
|
tools: List[dict] = None,
|
|
|
|
|
enabled_toolsets: List[str] = None,
|
|
|
|
|
session_id: str = None,
|
2026-03-05 16:09:57 -08:00
|
|
|
get_toolset_for_tool=None,
|
|
|
|
|
context_length: int = None):
|
2026-02-21 23:17:18 -08:00
|
|
|
"""Build and print a welcome banner with caduceus on left and info on right.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
console: Rich Console instance.
|
|
|
|
|
model: Current model name.
|
|
|
|
|
cwd: Current working directory.
|
|
|
|
|
tools: List of tool definitions.
|
|
|
|
|
enabled_toolsets: List of enabled toolset names.
|
|
|
|
|
session_id: Session identifier.
|
|
|
|
|
get_toolset_for_tool: Callable to map tool name -> toolset name.
|
2026-03-05 16:09:57 -08:00
|
|
|
context_length: Model's context window size in tokens.
|
2026-02-21 23:17:18 -08:00
|
|
|
"""
|
2026-03-29 16:53:29 -07:00
|
|
|
from model_tools import check_tool_availability, TOOLSET_REQUIREMENTS
|
2026-02-21 23:17:18 -08:00
|
|
|
if get_toolset_for_tool is None:
|
|
|
|
|
from model_tools import get_toolset_for_tool
|
|
|
|
|
|
|
|
|
|
tools = tools or []
|
|
|
|
|
enabled_toolsets = enabled_toolsets or []
|
|
|
|
|
|
|
|
|
|
_, unavailable_toolsets = check_tool_availability(quiet=True)
|
|
|
|
|
disabled_tools = set()
|
2026-03-29 16:53:29 -07:00
|
|
|
# Tools whose toolset has a check_fn are lazy-initialized (e.g. honcho,
|
|
|
|
|
# homeassistant) — they show as unavailable at banner time because the
|
|
|
|
|
# check hasn't run yet, but they aren't misconfigured.
|
|
|
|
|
lazy_tools = set()
|
2026-02-21 23:17:18 -08:00
|
|
|
for item in unavailable_toolsets:
|
2026-03-29 16:53:29 -07:00
|
|
|
toolset_name = item.get("name", "")
|
|
|
|
|
ts_req = TOOLSET_REQUIREMENTS.get(toolset_name, {})
|
|
|
|
|
tools_in_ts = item.get("tools", [])
|
|
|
|
|
if ts_req.get("check_fn"):
|
|
|
|
|
lazy_tools.update(tools_in_ts)
|
|
|
|
|
else:
|
|
|
|
|
disabled_tools.update(tools_in_ts)
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
layout_table = Table.grid(padding=(0, 2))
|
|
|
|
|
layout_table.add_column("left", justify="center")
|
|
|
|
|
layout_table.add_column("right", justify="left")
|
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
# Resolve skin colors once for the entire banner
|
|
|
|
|
accent = _skin_color("banner_accent", "#FFBF00")
|
|
|
|
|
dim = _skin_color("banner_dim", "#B8860B")
|
|
|
|
|
text = _skin_color("banner_text", "#FFF8DC")
|
|
|
|
|
session_color = _skin_color("session_border", "#8B8682")
|
|
|
|
|
|
2026-03-14 14:02:57 +07:00
|
|
|
# Use skin's custom caduceus art if provided
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.skin_engine import get_active_skin
|
|
|
|
|
_bskin = get_active_skin()
|
|
|
|
|
_hero = _bskin.banner_hero if hasattr(_bskin, 'banner_hero') and _bskin.banner_hero else HERMES_CADUCEUS
|
|
|
|
|
except Exception:
|
|
|
|
|
_bskin = None
|
|
|
|
|
_hero = HERMES_CADUCEUS
|
|
|
|
|
left_lines = ["", _hero, ""]
|
2026-02-21 23:17:18 -08:00
|
|
|
model_short = model.split("/")[-1] if "/" in model else model
|
2026-03-19 06:01:16 -07:00
|
|
|
if model_short.endswith(".gguf"):
|
|
|
|
|
model_short = model_short[:-5]
|
2026-02-21 23:17:18 -08:00
|
|
|
if len(model_short) > 28:
|
|
|
|
|
model_short = model_short[:25] + "..."
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
ctx_str = f" [dim {dim}]·[/] [dim {dim}]{_format_context_length(context_length)} context[/]" if context_length else ""
|
|
|
|
|
left_lines.append(f"[{accent}]{model_short}[/]{ctx_str} [dim {dim}]·[/] [dim {dim}]Nous Research[/]")
|
|
|
|
|
left_lines.append(f"[dim {dim}]{cwd}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
if session_id:
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
left_lines.append(f"[dim {session_color}]Session: {session_id}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
left_content = "\n".join(left_lines)
|
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
right_lines = [f"[bold {accent}]Available Tools[/]"]
|
2026-02-21 23:17:18 -08:00
|
|
|
toolsets_dict: Dict[str, list] = {}
|
|
|
|
|
|
|
|
|
|
for tool in tools:
|
|
|
|
|
tool_name = tool["function"]["name"]
|
2026-03-18 03:22:58 -07:00
|
|
|
toolset = _display_toolset_name(get_toolset_for_tool(tool_name) or "other")
|
2026-02-21 23:17:18 -08:00
|
|
|
toolsets_dict.setdefault(toolset, []).append(tool_name)
|
|
|
|
|
|
|
|
|
|
for item in unavailable_toolsets:
|
|
|
|
|
toolset_id = item.get("id", item.get("name", "unknown"))
|
2026-03-18 03:22:58 -07:00
|
|
|
display_name = _display_toolset_name(toolset_id)
|
2026-02-21 23:17:18 -08:00
|
|
|
if display_name not in toolsets_dict:
|
|
|
|
|
toolsets_dict[display_name] = []
|
|
|
|
|
for tool_name in item.get("tools", []):
|
|
|
|
|
if tool_name not in toolsets_dict[display_name]:
|
|
|
|
|
toolsets_dict[display_name].append(tool_name)
|
|
|
|
|
|
|
|
|
|
sorted_toolsets = sorted(toolsets_dict.keys())
|
|
|
|
|
display_toolsets = sorted_toolsets[:8]
|
|
|
|
|
remaining_toolsets = len(sorted_toolsets) - 8
|
|
|
|
|
|
|
|
|
|
for toolset in display_toolsets:
|
|
|
|
|
tool_names = toolsets_dict[toolset]
|
|
|
|
|
colored_names = []
|
|
|
|
|
for name in sorted(tool_names):
|
|
|
|
|
if name in disabled_tools:
|
|
|
|
|
colored_names.append(f"[red]{name}[/]")
|
2026-03-29 16:53:29 -07:00
|
|
|
elif name in lazy_tools:
|
|
|
|
|
colored_names.append(f"[yellow]{name}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
else:
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
colored_names.append(f"[{text}]{name}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
tools_str = ", ".join(colored_names)
|
|
|
|
|
if len(", ".join(sorted(tool_names))) > 45:
|
|
|
|
|
short_names = []
|
|
|
|
|
length = 0
|
|
|
|
|
for name in sorted(tool_names):
|
|
|
|
|
if length + len(name) + 2 > 42:
|
|
|
|
|
short_names.append("...")
|
|
|
|
|
break
|
|
|
|
|
short_names.append(name)
|
|
|
|
|
length += len(name) + 2
|
|
|
|
|
colored_names = []
|
|
|
|
|
for name in short_names:
|
|
|
|
|
if name == "...":
|
|
|
|
|
colored_names.append("[dim]...[/]")
|
|
|
|
|
elif name in disabled_tools:
|
|
|
|
|
colored_names.append(f"[red]{name}[/]")
|
2026-03-29 16:53:29 -07:00
|
|
|
elif name in lazy_tools:
|
|
|
|
|
colored_names.append(f"[yellow]{name}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
else:
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
colored_names.append(f"[{text}]{name}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
tools_str = ", ".join(colored_names)
|
|
|
|
|
|
2026-03-18 03:22:58 -07:00
|
|
|
right_lines.append(f"[dim {dim}]{toolset}:[/] {tools_str}")
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
if remaining_toolsets > 0:
|
2026-03-18 03:22:58 -07:00
|
|
|
right_lines.append(f"[dim {dim}](and {remaining_toolsets} more toolsets...)[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
|
feat(mcp): banner integration, /reload-mcp command, resources & prompts
Banner integration:
- MCP Servers section in CLI startup banner between Tools and Skills
- Shows each server with transport type, tool count, connection status
- Failed servers shown in red; section hidden when no MCP configured
- Summary line includes MCP server count
- Removed raw print() calls from discovery (banner handles display)
/reload-mcp command:
- New slash command in both CLI and gateway
- Disconnects all MCP servers, re-reads config.yaml, reconnects
- Reports what changed (added/removed/reconnected servers)
- Allows adding/removing MCP servers without restarting
Resources & Prompts support:
- 4 utility tools registered per server: list_resources, read_resource,
list_prompts, get_prompt
- Exposes MCP Resources (data sources) and Prompts (templates) as tools
- Proper parameter schemas (uri for read_resource, name for get_prompt)
- Handles text and binary resource content
- 23 new tests covering schemas, handlers, and registration
Test coverage: 74 MCP tests total, 1186 tests pass overall.
2026-03-02 19:15:59 -08:00
|
|
|
# MCP Servers section (only if configured)
|
|
|
|
|
try:
|
|
|
|
|
from tools.mcp_tool import get_mcp_status
|
|
|
|
|
mcp_status = get_mcp_status()
|
|
|
|
|
except Exception:
|
|
|
|
|
mcp_status = []
|
|
|
|
|
|
|
|
|
|
if mcp_status:
|
|
|
|
|
right_lines.append("")
|
2026-03-18 03:22:58 -07:00
|
|
|
right_lines.append(f"[bold {accent}]MCP Servers[/]")
|
feat(mcp): banner integration, /reload-mcp command, resources & prompts
Banner integration:
- MCP Servers section in CLI startup banner between Tools and Skills
- Shows each server with transport type, tool count, connection status
- Failed servers shown in red; section hidden when no MCP configured
- Summary line includes MCP server count
- Removed raw print() calls from discovery (banner handles display)
/reload-mcp command:
- New slash command in both CLI and gateway
- Disconnects all MCP servers, re-reads config.yaml, reconnects
- Reports what changed (added/removed/reconnected servers)
- Allows adding/removing MCP servers without restarting
Resources & Prompts support:
- 4 utility tools registered per server: list_resources, read_resource,
list_prompts, get_prompt
- Exposes MCP Resources (data sources) and Prompts (templates) as tools
- Proper parameter schemas (uri for read_resource, name for get_prompt)
- Handles text and binary resource content
- 23 new tests covering schemas, handlers, and registration
Test coverage: 74 MCP tests total, 1186 tests pass overall.
2026-03-02 19:15:59 -08:00
|
|
|
for srv in mcp_status:
|
|
|
|
|
if srv["connected"]:
|
|
|
|
|
right_lines.append(
|
2026-03-18 03:22:58 -07:00
|
|
|
f"[dim {dim}]{srv['name']}[/] [{text}]({srv['transport']})[/] "
|
|
|
|
|
f"[dim {dim}]—[/] [{text}]{srv['tools']} tool(s)[/]"
|
feat(mcp): banner integration, /reload-mcp command, resources & prompts
Banner integration:
- MCP Servers section in CLI startup banner between Tools and Skills
- Shows each server with transport type, tool count, connection status
- Failed servers shown in red; section hidden when no MCP configured
- Summary line includes MCP server count
- Removed raw print() calls from discovery (banner handles display)
/reload-mcp command:
- New slash command in both CLI and gateway
- Disconnects all MCP servers, re-reads config.yaml, reconnects
- Reports what changed (added/removed/reconnected servers)
- Allows adding/removing MCP servers without restarting
Resources & Prompts support:
- 4 utility tools registered per server: list_resources, read_resource,
list_prompts, get_prompt
- Exposes MCP Resources (data sources) and Prompts (templates) as tools
- Proper parameter schemas (uri for read_resource, name for get_prompt)
- Handles text and binary resource content
- 23 new tests covering schemas, handlers, and registration
Test coverage: 74 MCP tests total, 1186 tests pass overall.
2026-03-02 19:15:59 -08:00
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
right_lines.append(
|
|
|
|
|
f"[red]{srv['name']}[/] [dim]({srv['transport']})[/] "
|
|
|
|
|
f"[red]— failed[/]"
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-21 23:17:18 -08:00
|
|
|
right_lines.append("")
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
right_lines.append(f"[bold {accent}]Available Skills[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
skills_by_category = get_available_skills()
|
|
|
|
|
total_skills = sum(len(s) for s in skills_by_category.values())
|
|
|
|
|
|
|
|
|
|
if skills_by_category:
|
|
|
|
|
for category in sorted(skills_by_category.keys()):
|
|
|
|
|
skill_names = sorted(skills_by_category[category])
|
|
|
|
|
if len(skill_names) > 8:
|
|
|
|
|
display_names = skill_names[:8]
|
|
|
|
|
skills_str = ", ".join(display_names) + f" +{len(skill_names) - 8} more"
|
|
|
|
|
else:
|
|
|
|
|
skills_str = ", ".join(skill_names)
|
|
|
|
|
if len(skills_str) > 50:
|
|
|
|
|
skills_str = skills_str[:47] + "..."
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
right_lines.append(f"[dim {dim}]{category}:[/] [{text}]{skills_str}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
else:
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
right_lines.append(f"[dim {dim}]No skills installed[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
|
|
|
|
|
right_lines.append("")
|
feat(mcp): banner integration, /reload-mcp command, resources & prompts
Banner integration:
- MCP Servers section in CLI startup banner between Tools and Skills
- Shows each server with transport type, tool count, connection status
- Failed servers shown in red; section hidden when no MCP configured
- Summary line includes MCP server count
- Removed raw print() calls from discovery (banner handles display)
/reload-mcp command:
- New slash command in both CLI and gateway
- Disconnects all MCP servers, re-reads config.yaml, reconnects
- Reports what changed (added/removed/reconnected servers)
- Allows adding/removing MCP servers without restarting
Resources & Prompts support:
- 4 utility tools registered per server: list_resources, read_resource,
list_prompts, get_prompt
- Exposes MCP Resources (data sources) and Prompts (templates) as tools
- Proper parameter schemas (uri for read_resource, name for get_prompt)
- Handles text and binary resource content
- 23 new tests covering schemas, handlers, and registration
Test coverage: 74 MCP tests total, 1186 tests pass overall.
2026-03-02 19:15:59 -08:00
|
|
|
mcp_connected = sum(1 for s in mcp_status if s["connected"]) if mcp_status else 0
|
|
|
|
|
summary_parts = [f"{len(tools)} tools", f"{total_skills} skills"]
|
|
|
|
|
if mcp_connected:
|
|
|
|
|
summary_parts.append(f"{mcp_connected} MCP servers")
|
|
|
|
|
summary_parts.append("/help for commands")
|
feat: add profiles — run multiple isolated Hermes instances (#3681)
Each profile is a fully independent HERMES_HOME with its own config,
API keys, memory, sessions, skills, gateway, cron, and state.db.
Core module: hermes_cli/profiles.py (~900 lines)
- Profile CRUD: create, delete, list, show, rename
- Three clone levels: blank, --clone (config), --clone-all (everything)
- Export/import: tar.gz archive for backup and migration
- Wrapper alias scripts (~/.local/bin/<name>)
- Collision detection for alias names
- Sticky default via ~/.hermes/active_profile
- Skill seeding via subprocess (handles module-level caching)
- Auto-stop gateway on delete with disable-before-stop for services
- Tab completion generation for bash and zsh
CLI integration (hermes_cli/main.py):
- _apply_profile_override(): pre-import -p/--profile flag + sticky default
- Full 'hermes profile' subcommand: list, use, create, delete, show,
alias, rename, export, import
- 'hermes completion bash/zsh' command
- Multi-profile skill sync in hermes update
Display (cli.py, banner.py, gateway/run.py):
- CLI prompt: 'coder ❯' when using a non-default profile
- Banner shows profile name
- Gateway startup log includes profile name
Gateway safety:
- Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern)
- Port conflict detection: API server, webhook adapter
Diagnostics (hermes_cli/doctor.py):
- Profile health section: lists profiles, checks config, .env, aliases
- Orphan alias detection: warns when wrapper points to deleted profile
Tests (tests/hermes_cli/test_profiles.py):
- 71 automated tests covering: validation, CRUD, clone levels, rename,
export/import, active profile, isolation, alias collision, completion
- Full suite: 6760 passed, 0 new failures
Documentation:
- website/docs/user-guide/profiles.md: full user guide (12 sections)
- website/docs/reference/profile-commands.md: command reference (12 commands)
- website/docs/reference/faq.md: 6 profile FAQ entries
- website/sidebars.ts: navigation updated
2026-03-29 10:41:20 -07:00
|
|
|
# Show active profile name when not 'default'
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.profiles import get_active_profile_name
|
|
|
|
|
_profile_name = get_active_profile_name()
|
|
|
|
|
if _profile_name and _profile_name != "default":
|
|
|
|
|
right_lines.append(f"[bold {accent}]Profile:[/] [{text}]{_profile_name}[/]")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # Never break the banner over a profiles.py bug
|
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
right_lines.append(f"[dim {dim}]{' · '.join(summary_parts)}[/]")
|
2026-02-21 23:17:18 -08:00
|
|
|
|
2026-03-14 14:02:57 +07:00
|
|
|
# Update check — use prefetched result if available
|
2026-03-07 07:35:36 -08:00
|
|
|
try:
|
2026-03-14 14:02:57 +07:00
|
|
|
behind = get_update_result(timeout=0.5)
|
2026-03-07 07:35:36 -08:00
|
|
|
if behind and behind > 0:
|
|
|
|
|
commits_word = "commit" if behind == 1 else "commits"
|
|
|
|
|
right_lines.append(
|
|
|
|
|
f"[bold yellow]⚠ {behind} {commits_word} behind[/]"
|
|
|
|
|
f"[dim yellow] — run [bold]hermes update[/bold] to update[/]"
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # Never break the banner over an update check
|
|
|
|
|
|
2026-02-21 23:17:18 -08:00
|
|
|
right_content = "\n".join(right_lines)
|
|
|
|
|
layout_table.add_row(left_content, right_content)
|
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
agent_name = _skin_branding("agent_name", "Hermes Agent")
|
|
|
|
|
title_color = _skin_color("banner_title", "#FFD700")
|
|
|
|
|
border_color = _skin_color("banner_border", "#CD7F32")
|
2026-02-21 23:17:18 -08:00
|
|
|
outer_panel = Panel(
|
|
|
|
|
layout_table,
|
2026-03-12 01:35:47 -07:00
|
|
|
title=f"[bold {title_color}]{agent_name} v{VERSION} ({RELEASE_DATE})[/]",
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
border_style=border_color,
|
2026-02-21 23:17:18 -08:00
|
|
|
padding=(0, 2),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.print()
|
2026-03-14 14:02:57 +07:00
|
|
|
term_width = shutil.get_terminal_size().columns
|
|
|
|
|
if term_width >= 95:
|
|
|
|
|
_logo = _bskin.banner_logo if _bskin and hasattr(_bskin, 'banner_logo') and _bskin.banner_logo else HERMES_AGENT_LOGO
|
|
|
|
|
console.print(_logo)
|
|
|
|
|
console.print()
|
2026-02-21 23:17:18 -08:00
|
|
|
console.print(outer_panel)
|