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
|
2026-03-07 07:35:36 -08:00
|
|
|
from typing import Dict, List, Any, 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
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
|
|
|
|
_GOLD = "\033[1;33m"
|
|
|
|
|
_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]]:
|
|
|
|
|
"""Scan ~/.hermes/skills/ and return skills grouped by category."""
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
|
|
|
skills_dir = hermes_home / "skills"
|
|
|
|
|
skills_by_category = {}
|
|
|
|
|
|
|
|
|
|
if not skills_dir.exists():
|
|
|
|
|
return skills_by_category
|
|
|
|
|
|
|
|
|
|
for skill_file in skills_dir.rglob("SKILL.md"):
|
|
|
|
|
rel_path = skill_file.relative_to(skills_dir)
|
|
|
|
|
parts = rel_path.parts
|
|
|
|
|
if len(parts) >= 2:
|
|
|
|
|
category = parts[0]
|
|
|
|
|
skill_name = parts[-2]
|
|
|
|
|
else:
|
|
|
|
|
category = "general"
|
|
|
|
|
skill_name = skill_file.parent.name
|
|
|
|
|
skills_by_category.setdefault(category, []).append(skill_name)
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
"""
|
|
|
|
|
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
|
|
|
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-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
|
|
|
"""
|
|
|
|
|
from model_tools import check_tool_availability, TOOLSET_REQUIREMENTS
|
|
|
|
|
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()
|
|
|
|
|
for item in unavailable_toolsets:
|
|
|
|
|
disabled_tools.update(item.get("tools", []))
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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"]
|
|
|
|
|
toolset = get_toolset_for_tool(tool_name) or "other"
|
|
|
|
|
toolsets_dict.setdefault(toolset, []).append(tool_name)
|
|
|
|
|
|
|
|
|
|
for item in unavailable_toolsets:
|
|
|
|
|
toolset_id = item.get("id", item.get("name", "unknown"))
|
|
|
|
|
display_name = f"{toolset_id}_tools" if not toolset_id.endswith("_tools") else toolset_id
|
|
|
|
|
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}[/]")
|
|
|
|
|
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}[/]")
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
right_lines.append(f"[dim #B8860B]{toolset}:[/] {tools_str}")
|
|
|
|
|
|
|
|
|
|
if remaining_toolsets > 0:
|
|
|
|
|
right_lines.append(f"[dim #B8860B](and {remaining_toolsets} more toolsets...)[/]")
|
|
|
|
|
|
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("")
|
|
|
|
|
right_lines.append("[bold #FFBF00]MCP Servers[/]")
|
|
|
|
|
for srv in mcp_status:
|
|
|
|
|
if srv["connected"]:
|
|
|
|
|
right_lines.append(
|
|
|
|
|
f"[dim #B8860B]{srv['name']}[/] [#FFF8DC]({srv['transport']})[/] "
|
|
|
|
|
f"[dim #B8860B]—[/] [#FFF8DC]{srv['tools']} tool(s)[/]"
|
|
|
|
|
)
|
|
|
|
|
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 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)
|