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
|
|
|
|
"""Hermes CLI skin/theme engine.
|
|
|
|
|
|
|
|
|
|
|
|
A data-driven skin system that lets users customize the CLI's visual appearance.
|
|
|
|
|
|
Skins are defined as YAML files in ~/.hermes/skins/ or as built-in presets.
|
2026-03-10 00:51:27 -07:00
|
|
|
|
No code changes are needed to add a new skin.
|
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
|
|
|
|
|
2026-03-10 00:51:27 -07:00
|
|
|
|
SKIN YAML SCHEMA
|
|
|
|
|
|
================
|
|
|
|
|
|
|
|
|
|
|
|
All fields are optional. Missing values inherit from the ``default`` skin.
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: yaml
|
|
|
|
|
|
|
|
|
|
|
|
# Required: skin identity
|
|
|
|
|
|
name: mytheme # Unique skin name (lowercase, hyphens ok)
|
|
|
|
|
|
description: Short description # Shown in /skin listing
|
|
|
|
|
|
|
|
|
|
|
|
# Colors: hex values for Rich markup (banner, UI, response box)
|
|
|
|
|
|
colors:
|
|
|
|
|
|
banner_border: "#CD7F32" # Panel border color
|
|
|
|
|
|
banner_title: "#FFD700" # Panel title text color
|
|
|
|
|
|
banner_accent: "#FFBF00" # Section headers (Available Tools, etc.)
|
|
|
|
|
|
banner_dim: "#B8860B" # Dim/muted text (separators, labels)
|
|
|
|
|
|
banner_text: "#FFF8DC" # Body text (tool names, skill names)
|
|
|
|
|
|
ui_accent: "#FFBF00" # General UI accent
|
|
|
|
|
|
ui_label: "#4dd0e1" # UI labels
|
|
|
|
|
|
ui_ok: "#4caf50" # Success indicators
|
|
|
|
|
|
ui_error: "#ef5350" # Error indicators
|
|
|
|
|
|
ui_warn: "#ffa726" # Warning indicators
|
|
|
|
|
|
prompt: "#FFF8DC" # Prompt text color
|
|
|
|
|
|
input_rule: "#CD7F32" # Input area horizontal rule
|
|
|
|
|
|
response_border: "#FFD700" # Response box border (ANSI)
|
|
|
|
|
|
session_label: "#DAA520" # Session label color
|
|
|
|
|
|
session_border: "#8B8682" # Session ID dim color
|
|
|
|
|
|
|
|
|
|
|
|
# Spinner: customize the animated spinner during API calls
|
|
|
|
|
|
spinner:
|
|
|
|
|
|
waiting_faces: # Faces shown while waiting for API
|
|
|
|
|
|
- "(⚔)"
|
|
|
|
|
|
- "(⛨)"
|
|
|
|
|
|
thinking_faces: # Faces shown during reasoning
|
|
|
|
|
|
- "(⌁)"
|
|
|
|
|
|
- "(<>)"
|
|
|
|
|
|
thinking_verbs: # Verbs for spinner messages
|
|
|
|
|
|
- "forging"
|
|
|
|
|
|
- "plotting"
|
|
|
|
|
|
wings: # Optional left/right spinner decorations
|
|
|
|
|
|
- ["⟪⚔", "⚔⟫"] # Each entry is [left, right] pair
|
|
|
|
|
|
- ["⟪▲", "▲⟫"]
|
|
|
|
|
|
|
|
|
|
|
|
# Branding: text strings used throughout the CLI
|
|
|
|
|
|
branding:
|
|
|
|
|
|
agent_name: "Hermes Agent" # Banner title, status display
|
|
|
|
|
|
welcome: "Welcome message" # Shown at CLI startup
|
|
|
|
|
|
goodbye: "Goodbye! ⚕" # Shown on exit
|
|
|
|
|
|
response_label: " ⚕ Hermes " # Response box header label
|
|
|
|
|
|
prompt_symbol: "❯ " # Input prompt symbol
|
|
|
|
|
|
help_header: "(^_^)? Commands" # /help header text
|
|
|
|
|
|
|
|
|
|
|
|
# Tool prefix: character for tool output lines (default: ┊)
|
|
|
|
|
|
tool_prefix: "┊"
|
|
|
|
|
|
|
|
|
|
|
|
USAGE
|
|
|
|
|
|
=====
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
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
|
|
|
|
|
|
|
|
|
|
from hermes_cli.skin_engine import get_active_skin, list_skins, set_active_skin
|
|
|
|
|
|
|
|
|
|
|
|
skin = get_active_skin()
|
2026-03-10 00:51:27 -07:00
|
|
|
|
print(skin.colors["banner_title"]) # "#FFD700"
|
|
|
|
|
|
print(skin.get_branding("agent_name")) # "Hermes Agent"
|
|
|
|
|
|
|
|
|
|
|
|
set_active_skin("ares") # Switch to built-in ares skin
|
|
|
|
|
|
set_active_skin("mytheme") # Switch to user skin from ~/.hermes/skins/
|
|
|
|
|
|
|
|
|
|
|
|
BUILT-IN SKINS
|
|
|
|
|
|
==============
|
|
|
|
|
|
|
|
|
|
|
|
- ``default`` — Classic Hermes gold/kawaii (the current look)
|
|
|
|
|
|
- ``ares`` — Crimson/bronze war-god theme with custom spinner wings
|
|
|
|
|
|
- ``mono`` — Clean grayscale monochrome
|
|
|
|
|
|
- ``slate`` — Cool blue developer-focused theme
|
|
|
|
|
|
|
|
|
|
|
|
USER SKINS
|
|
|
|
|
|
==========
|
|
|
|
|
|
|
|
|
|
|
|
Drop a YAML file in ``~/.hermes/skins/<name>.yaml`` following the schema above.
|
|
|
|
|
|
Activate with ``/skin <name>`` in the CLI or ``display.skin: <name>`` in config.yaml.
|
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
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import os
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Skin data structure
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class SkinConfig:
|
|
|
|
|
|
"""Complete skin configuration."""
|
|
|
|
|
|
name: str
|
|
|
|
|
|
description: str = ""
|
|
|
|
|
|
colors: Dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
spinner: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
branding: Dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
tool_prefix: str = "┊"
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
banner_logo: str = "" # Rich-markup ASCII art logo (replaces HERMES_AGENT_LOGO)
|
|
|
|
|
|
banner_hero: str = "" # Rich-markup hero art (replaces HERMES_CADUCEUS)
|
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
|
|
|
|
|
|
|
|
|
|
def get_color(self, key: str, fallback: str = "") -> str:
|
|
|
|
|
|
"""Get a color value with fallback."""
|
|
|
|
|
|
return self.colors.get(key, fallback)
|
|
|
|
|
|
|
|
|
|
|
|
def get_spinner_list(self, key: str) -> List[str]:
|
|
|
|
|
|
"""Get a spinner list (faces, verbs, etc.)."""
|
|
|
|
|
|
return self.spinner.get(key, [])
|
|
|
|
|
|
|
|
|
|
|
|
def get_spinner_wings(self) -> List[Tuple[str, str]]:
|
|
|
|
|
|
"""Get spinner wing pairs, or empty list if none."""
|
|
|
|
|
|
raw = self.spinner.get("wings", [])
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for pair in raw:
|
|
|
|
|
|
if isinstance(pair, (list, tuple)) and len(pair) == 2:
|
|
|
|
|
|
result.append((str(pair[0]), str(pair[1])))
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def get_branding(self, key: str, fallback: str = "") -> str:
|
|
|
|
|
|
"""Get a branding value with fallback."""
|
|
|
|
|
|
return self.branding.get(key, fallback)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Built-in skin definitions
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
_BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|
|
|
|
|
"default": {
|
|
|
|
|
|
"name": "default",
|
|
|
|
|
|
"description": "Classic Hermes — gold and kawaii",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#CD7F32",
|
|
|
|
|
|
"banner_title": "#FFD700",
|
|
|
|
|
|
"banner_accent": "#FFBF00",
|
|
|
|
|
|
"banner_dim": "#B8860B",
|
|
|
|
|
|
"banner_text": "#FFF8DC",
|
|
|
|
|
|
"ui_accent": "#FFBF00",
|
|
|
|
|
|
"ui_label": "#4dd0e1",
|
|
|
|
|
|
"ui_ok": "#4caf50",
|
|
|
|
|
|
"ui_error": "#ef5350",
|
|
|
|
|
|
"ui_warn": "#ffa726",
|
|
|
|
|
|
"prompt": "#FFF8DC",
|
|
|
|
|
|
"input_rule": "#CD7F32",
|
|
|
|
|
|
"response_border": "#FFD700",
|
|
|
|
|
|
"session_label": "#DAA520",
|
|
|
|
|
|
"session_border": "#8B8682",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {
|
|
|
|
|
|
# Empty = use hardcoded defaults in display.py
|
|
|
|
|
|
},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Hermes Agent",
|
|
|
|
|
|
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "Goodbye! ⚕",
|
|
|
|
|
|
"response_label": " ⚕ Hermes ",
|
|
|
|
|
|
"prompt_symbol": "❯ ",
|
|
|
|
|
|
"help_header": "(^_^)? Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "┊",
|
|
|
|
|
|
},
|
|
|
|
|
|
"ares": {
|
|
|
|
|
|
"name": "ares",
|
|
|
|
|
|
"description": "War-god theme — crimson and bronze",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#9F1C1C",
|
|
|
|
|
|
"banner_title": "#C7A96B",
|
|
|
|
|
|
"banner_accent": "#DD4A3A",
|
|
|
|
|
|
"banner_dim": "#6B1717",
|
|
|
|
|
|
"banner_text": "#F1E6CF",
|
|
|
|
|
|
"ui_accent": "#DD4A3A",
|
|
|
|
|
|
"ui_label": "#C7A96B",
|
|
|
|
|
|
"ui_ok": "#4caf50",
|
|
|
|
|
|
"ui_error": "#ef5350",
|
|
|
|
|
|
"ui_warn": "#ffa726",
|
|
|
|
|
|
"prompt": "#F1E6CF",
|
|
|
|
|
|
"input_rule": "#9F1C1C",
|
|
|
|
|
|
"response_border": "#C7A96B",
|
|
|
|
|
|
"session_label": "#C7A96B",
|
|
|
|
|
|
"session_border": "#6E584B",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {
|
|
|
|
|
|
"waiting_faces": ["(⚔)", "(⛨)", "(▲)", "(<>)", "(/)"],
|
|
|
|
|
|
"thinking_faces": ["(⚔)", "(⛨)", "(▲)", "(⌁)", "(<>)"],
|
|
|
|
|
|
"thinking_verbs": [
|
|
|
|
|
|
"forging", "marching", "sizing the field", "holding the line",
|
|
|
|
|
|
"hammering plans", "tempering steel", "plotting impact", "raising the shield",
|
|
|
|
|
|
],
|
|
|
|
|
|
"wings": [
|
|
|
|
|
|
["⟪⚔", "⚔⟫"],
|
|
|
|
|
|
["⟪▲", "▲⟫"],
|
|
|
|
|
|
["⟪╸", "╺⟫"],
|
|
|
|
|
|
["⟪⛨", "⛨⟫"],
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Ares Agent",
|
|
|
|
|
|
"welcome": "Welcome to Ares Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "Farewell, warrior! ⚔",
|
|
|
|
|
|
"response_label": " ⚔ Ares ",
|
|
|
|
|
|
"prompt_symbol": "⚔ ❯ ",
|
|
|
|
|
|
"help_header": "(⚔) Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "╎",
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
"banner_logo": """[bold #A3261F] █████╗ ██████╗ ███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
|
|
|
|
|
[bold #B73122]██╔══██╗██╔══██╗██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
|
|
|
|
|
[#C93C24]███████║██████╔╝█████╗ ███████╗█████╗███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║[/]
|
|
|
|
|
|
[#D84A28]██╔══██║██╔══██╗██╔══╝ ╚════██║╚════╝██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║[/]
|
|
|
|
|
|
[#E15A2D]██║ ██║██║ ██║███████╗███████║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║[/]
|
|
|
|
|
|
[#EB6C32]╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝[/]""",
|
2026-03-10 03:54:12 -07:00
|
|
|
|
"banner_hero": """[#9F1C1C]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#9F1C1C]⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠟⠻⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#C7A96B]⠀⠀⠀⠀⠀⠀⠀⣠⣾⡿⠋⠀⠀⠀⠙⢿⣷⣄⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#C7A96B]⠀⠀⠀⠀⠀⢀⣾⡿⠋⠀⠀⢠⡄⠀⠀⠙⢿⣷⡀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#DD4A3A]⠀⠀⠀⠀⣰⣿⠟⠀⠀⠀⣰⣿⣿⣆⠀⠀⠀⠻⣿⣆⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#DD4A3A]⠀⠀⠀⢰⣿⠏⠀⠀⢀⣾⡿⠉⢿⣷⡀⠀⠀⠹⣿⡆⠀⠀⠀[/]
|
|
|
|
|
|
[#9F1C1C]⠀⠀⠀⣿⡟⠀⠀⣠⣿⠟⠀⠀⠀⠻⣿⣄⠀⠀⢻⣿⠀⠀⠀[/]
|
|
|
|
|
|
[#9F1C1C]⠀⠀⠀⣿⡇⠀⠀⠙⠋⠀⠀⚔⠀⠀⠙⠋⠀⠀⢸⣿⠀⠀⠀[/]
|
|
|
|
|
|
[#6B1717]⠀⠀⠀⢿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⡿⠀⠀⠀[/]
|
|
|
|
|
|
[#6B1717]⠀⠀⠀⠘⢿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⡿⠃⠀⠀⠀[/]
|
|
|
|
|
|
[#C7A96B]⠀⠀⠀⠀⠈⠻⣿⣷⣦⣤⣀⣀⣤⣤⣶⣿⠿⠋⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#C7A96B]⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⠿⠿⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#DD4A3A]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⚔⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[dim #6B1717]⠀⠀⠀⠀⠀⠀⠀⠀war god online⠀⠀⠀⠀⠀⠀⠀⠀[/]""",
|
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
|
|
|
|
},
|
|
|
|
|
|
"mono": {
|
|
|
|
|
|
"name": "mono",
|
|
|
|
|
|
"description": "Monochrome — clean grayscale",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#555555",
|
|
|
|
|
|
"banner_title": "#e6edf3",
|
|
|
|
|
|
"banner_accent": "#aaaaaa",
|
|
|
|
|
|
"banner_dim": "#444444",
|
|
|
|
|
|
"banner_text": "#c9d1d9",
|
|
|
|
|
|
"ui_accent": "#aaaaaa",
|
|
|
|
|
|
"ui_label": "#888888",
|
|
|
|
|
|
"ui_ok": "#888888",
|
|
|
|
|
|
"ui_error": "#cccccc",
|
|
|
|
|
|
"ui_warn": "#999999",
|
|
|
|
|
|
"prompt": "#c9d1d9",
|
|
|
|
|
|
"input_rule": "#444444",
|
|
|
|
|
|
"response_border": "#aaaaaa",
|
|
|
|
|
|
"session_label": "#888888",
|
|
|
|
|
|
"session_border": "#555555",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Hermes Agent",
|
|
|
|
|
|
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "Goodbye! ⚕",
|
|
|
|
|
|
"response_label": " ⚕ Hermes ",
|
|
|
|
|
|
"prompt_symbol": "❯ ",
|
|
|
|
|
|
"help_header": "[?] Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "┊",
|
|
|
|
|
|
},
|
|
|
|
|
|
"slate": {
|
|
|
|
|
|
"name": "slate",
|
|
|
|
|
|
"description": "Cool blue — developer-focused",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#4169e1",
|
|
|
|
|
|
"banner_title": "#7eb8f6",
|
|
|
|
|
|
"banner_accent": "#8EA8FF",
|
|
|
|
|
|
"banner_dim": "#4b5563",
|
|
|
|
|
|
"banner_text": "#c9d1d9",
|
|
|
|
|
|
"ui_accent": "#7eb8f6",
|
|
|
|
|
|
"ui_label": "#8EA8FF",
|
|
|
|
|
|
"ui_ok": "#63D0A6",
|
|
|
|
|
|
"ui_error": "#F7A072",
|
|
|
|
|
|
"ui_warn": "#e6a855",
|
|
|
|
|
|
"prompt": "#c9d1d9",
|
|
|
|
|
|
"input_rule": "#4169e1",
|
|
|
|
|
|
"response_border": "#7eb8f6",
|
|
|
|
|
|
"session_label": "#7eb8f6",
|
|
|
|
|
|
"session_border": "#4b5563",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Hermes Agent",
|
|
|
|
|
|
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "Goodbye! ⚕",
|
|
|
|
|
|
"response_label": " ⚕ Hermes ",
|
|
|
|
|
|
"prompt_symbol": "❯ ",
|
|
|
|
|
|
"help_header": "(^_^)? Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "┊",
|
|
|
|
|
|
},
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
"poseidon": {
|
|
|
|
|
|
"name": "poseidon",
|
|
|
|
|
|
"description": "Ocean-god theme — deep blue and seafoam",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#2A6FB9",
|
|
|
|
|
|
"banner_title": "#A9DFFF",
|
|
|
|
|
|
"banner_accent": "#5DB8F5",
|
|
|
|
|
|
"banner_dim": "#153C73",
|
|
|
|
|
|
"banner_text": "#EAF7FF",
|
|
|
|
|
|
"ui_accent": "#5DB8F5",
|
|
|
|
|
|
"ui_label": "#A9DFFF",
|
|
|
|
|
|
"ui_ok": "#4caf50",
|
|
|
|
|
|
"ui_error": "#ef5350",
|
|
|
|
|
|
"ui_warn": "#ffa726",
|
|
|
|
|
|
"prompt": "#EAF7FF",
|
|
|
|
|
|
"input_rule": "#2A6FB9",
|
|
|
|
|
|
"response_border": "#5DB8F5",
|
|
|
|
|
|
"session_label": "#A9DFFF",
|
|
|
|
|
|
"session_border": "#496884",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {
|
|
|
|
|
|
"waiting_faces": ["(≈)", "(Ψ)", "(∿)", "(◌)", "(◠)"],
|
|
|
|
|
|
"thinking_faces": ["(Ψ)", "(∿)", "(≈)", "(⌁)", "(◌)"],
|
|
|
|
|
|
"thinking_verbs": [
|
|
|
|
|
|
"charting currents", "sounding the depth", "reading foam lines",
|
|
|
|
|
|
"steering the trident", "tracking undertow", "plotting sea lanes",
|
|
|
|
|
|
"calling the swell", "measuring pressure",
|
|
|
|
|
|
],
|
|
|
|
|
|
"wings": [
|
|
|
|
|
|
["⟪≈", "≈⟫"],
|
|
|
|
|
|
["⟪Ψ", "Ψ⟫"],
|
|
|
|
|
|
["⟪∿", "∿⟫"],
|
|
|
|
|
|
["⟪◌", "◌⟫"],
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Poseidon Agent",
|
|
|
|
|
|
"welcome": "Welcome to Poseidon Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "Fair winds! Ψ",
|
|
|
|
|
|
"response_label": " Ψ Poseidon ",
|
|
|
|
|
|
"prompt_symbol": "Ψ ❯ ",
|
|
|
|
|
|
"help_header": "(Ψ) Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "│",
|
|
|
|
|
|
"banner_logo": """[bold #B8E8FF]██████╗ ██████╗ ███████╗██╗██████╗ ███████╗ ██████╗ ███╗ ██╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
|
|
|
|
|
[bold #97D6FF]██╔══██╗██╔═══██╗██╔════╝██║██╔══██╗██╔════╝██╔═══██╗████╗ ██║ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
|
|
|
|
|
[#75C1F6]██████╔╝██║ ██║███████╗██║██║ ██║█████╗ ██║ ██║██╔██╗ ██║█████╗███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║[/]
|
|
|
|
|
|
[#4FA2E0]██╔═══╝ ██║ ██║╚════██║██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║╚════╝██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║[/]
|
|
|
|
|
|
[#2E7CC7]██║ ╚██████╔╝███████║██║██████╔╝███████╗╚██████╔╝██║ ╚████║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║[/]
|
|
|
|
|
|
[#1B4F95]╚═╝ ╚═════╝ ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝[/]""",
|
2026-03-10 03:54:12 -07:00
|
|
|
|
"banner_hero": """[#2A6FB9]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#5DB8F5]⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#5DB8F5]⠀⠀⠀⠀⠀⠀⠀⢠⣿⠏⠀Ψ⠀⠹⣿⡄⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#A9DFFF]⠀⠀⠀⠀⠀⠀⠀⣿⡟⠀⠀⠀⠀⠀⢻⣿⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#A9DFFF]⠀⠀⠀≈≈≈≈≈⣿⡇⠀⠀⠀⠀⠀⢸⣿≈≈≈≈≈⠀⠀⠀[/]
|
|
|
|
|
|
[#5DB8F5]⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#2A6FB9]⠀⠀⠀⠀⠀⠀⠀⢿⣧⠀⠀⠀⠀⠀⣼⡿⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#2A6FB9]⠀⠀⠀⠀⠀⠀⠀⠘⢿⣷⣄⣀⣠⣾⡿⠃⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#153C73]⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#153C73]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#5DB8F5]⠀⠀⠀⠀⠀≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#A9DFFF]⠀⠀⠀⠀⠀⠀≈≈≈≈≈≈≈≈≈≈≈≈≈⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[dim #153C73]⠀⠀⠀⠀⠀⠀⠀deep waters hold⠀⠀⠀⠀⠀⠀⠀[/]""",
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
},
|
|
|
|
|
|
"sisyphus": {
|
|
|
|
|
|
"name": "sisyphus",
|
|
|
|
|
|
"description": "Sisyphean theme — austere grayscale with persistence",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#B7B7B7",
|
|
|
|
|
|
"banner_title": "#F5F5F5",
|
|
|
|
|
|
"banner_accent": "#E7E7E7",
|
|
|
|
|
|
"banner_dim": "#4A4A4A",
|
|
|
|
|
|
"banner_text": "#D3D3D3",
|
|
|
|
|
|
"ui_accent": "#E7E7E7",
|
|
|
|
|
|
"ui_label": "#D3D3D3",
|
|
|
|
|
|
"ui_ok": "#919191",
|
|
|
|
|
|
"ui_error": "#E7E7E7",
|
|
|
|
|
|
"ui_warn": "#B7B7B7",
|
|
|
|
|
|
"prompt": "#F5F5F5",
|
|
|
|
|
|
"input_rule": "#656565",
|
|
|
|
|
|
"response_border": "#B7B7B7",
|
|
|
|
|
|
"session_label": "#919191",
|
|
|
|
|
|
"session_border": "#656565",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {
|
|
|
|
|
|
"waiting_faces": ["(◉)", "(◌)", "(◬)", "(⬤)", "(::)"],
|
|
|
|
|
|
"thinking_faces": ["(◉)", "(◬)", "(◌)", "(○)", "(●)"],
|
|
|
|
|
|
"thinking_verbs": [
|
|
|
|
|
|
"finding traction", "measuring the grade", "resetting the boulder",
|
|
|
|
|
|
"counting the ascent", "testing leverage", "setting the shoulder",
|
|
|
|
|
|
"pushing uphill", "enduring the loop",
|
|
|
|
|
|
],
|
|
|
|
|
|
"wings": [
|
|
|
|
|
|
["⟪◉", "◉⟫"],
|
|
|
|
|
|
["⟪◬", "◬⟫"],
|
|
|
|
|
|
["⟪◌", "◌⟫"],
|
|
|
|
|
|
["⟪⬤", "⬤⟫"],
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Sisyphus Agent",
|
|
|
|
|
|
"welcome": "Welcome to Sisyphus Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "The boulder waits. ◉",
|
|
|
|
|
|
"response_label": " ◉ Sisyphus ",
|
|
|
|
|
|
"prompt_symbol": "◉ ❯ ",
|
|
|
|
|
|
"help_header": "(◉) Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "│",
|
|
|
|
|
|
"banner_logo": """[bold #F5F5F5]███████╗██╗███████╗██╗ ██╗██████╗ ██╗ ██╗██╗ ██╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
|
|
|
|
|
[bold #E7E7E7]██╔════╝██║██╔════╝╚██╗ ██╔╝██╔══██╗██║ ██║██║ ██║██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
|
|
|
|
|
[#D7D7D7]███████╗██║███████╗ ╚████╔╝ ██████╔╝███████║██║ ██║███████╗█████╗███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║[/]
|
|
|
|
|
|
[#BFBFBF]╚════██║██║╚════██║ ╚██╔╝ ██╔═══╝ ██╔══██║██║ ██║╚════██║╚════╝██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║[/]
|
|
|
|
|
|
[#8F8F8F]███████║██║███████║ ██║ ██║ ██║ ██║╚██████╔╝███████║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║[/]
|
|
|
|
|
|
[#626262]╚══════╝╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝[/]""",
|
2026-03-10 03:54:12 -07:00
|
|
|
|
"banner_hero": """[#B7B7B7]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#D3D3D3]⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#E7E7E7]⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#F5F5F5]⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#E7E7E7]⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#D3D3D3]⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#B7B7B7]⠀⠀⠀⠀⠀⠀⠀⠀⠙⠿⣿⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#919191]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#656565]⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#656565]⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#4A4A4A]⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#4A4A4A]⠀⠀⠀⠀⠀⣀⣴⣿⣿⣿⣿⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#656565]⠀⠀⠀━━━━━━━━━━━━━━━━━━━━━━━⠀⠀⠀[/]
|
|
|
|
|
|
[dim #4A4A4A]⠀⠀⠀⠀⠀⠀⠀⠀⠀the boulder⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]""",
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
},
|
|
|
|
|
|
"charizard": {
|
|
|
|
|
|
"name": "charizard",
|
|
|
|
|
|
"description": "Volcanic theme — burnt orange and ember",
|
|
|
|
|
|
"colors": {
|
|
|
|
|
|
"banner_border": "#C75B1D",
|
|
|
|
|
|
"banner_title": "#FFD39A",
|
|
|
|
|
|
"banner_accent": "#F29C38",
|
|
|
|
|
|
"banner_dim": "#7A3511",
|
|
|
|
|
|
"banner_text": "#FFF0D4",
|
|
|
|
|
|
"ui_accent": "#F29C38",
|
|
|
|
|
|
"ui_label": "#FFD39A",
|
|
|
|
|
|
"ui_ok": "#4caf50",
|
|
|
|
|
|
"ui_error": "#ef5350",
|
|
|
|
|
|
"ui_warn": "#ffa726",
|
|
|
|
|
|
"prompt": "#FFF0D4",
|
|
|
|
|
|
"input_rule": "#C75B1D",
|
|
|
|
|
|
"response_border": "#F29C38",
|
|
|
|
|
|
"session_label": "#FFD39A",
|
|
|
|
|
|
"session_border": "#6C4724",
|
|
|
|
|
|
},
|
|
|
|
|
|
"spinner": {
|
|
|
|
|
|
"waiting_faces": ["(✦)", "(▲)", "(◇)", "(<>)", "(🔥)"],
|
|
|
|
|
|
"thinking_faces": ["(✦)", "(▲)", "(◇)", "(⌁)", "(🔥)"],
|
|
|
|
|
|
"thinking_verbs": [
|
|
|
|
|
|
"banking into the draft", "measuring burn", "reading the updraft",
|
|
|
|
|
|
"tracking ember fall", "setting wing angle", "holding the flame core",
|
|
|
|
|
|
"plotting a hot landing", "coiling for lift",
|
|
|
|
|
|
],
|
|
|
|
|
|
"wings": [
|
|
|
|
|
|
["⟪✦", "✦⟫"],
|
|
|
|
|
|
["⟪▲", "▲⟫"],
|
|
|
|
|
|
["⟪◌", "◌⟫"],
|
|
|
|
|
|
["⟪◇", "◇⟫"],
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
"branding": {
|
|
|
|
|
|
"agent_name": "Charizard Agent",
|
|
|
|
|
|
"welcome": "Welcome to Charizard Agent! Type your message or /help for commands.",
|
|
|
|
|
|
"goodbye": "Flame out! ✦",
|
|
|
|
|
|
"response_label": " ✦ Charizard ",
|
|
|
|
|
|
"prompt_symbol": "✦ ❯ ",
|
|
|
|
|
|
"help_header": "(✦) Available Commands",
|
|
|
|
|
|
},
|
|
|
|
|
|
"tool_prefix": "│",
|
|
|
|
|
|
"banner_logo": """[bold #FFF0D4] ██████╗██╗ ██╗ █████╗ ██████╗ ██╗███████╗ █████╗ ██████╗ ██████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
|
|
|
|
|
[bold #FFD39A]██╔════╝██║ ██║██╔══██╗██╔══██╗██║╚══███╔╝██╔══██╗██╔══██╗██╔══██╗ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
|
|
|
|
|
[#F29C38]██║ ███████║███████║██████╔╝██║ ███╔╝ ███████║██████╔╝██║ ██║█████╗███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║[/]
|
|
|
|
|
|
[#E2832B]██║ ██╔══██║██╔══██║██╔══██╗██║ ███╔╝ ██╔══██║██╔══██╗██║ ██║╚════╝██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║[/]
|
|
|
|
|
|
[#C75B1D]╚██████╗██║ ██║██║ ██║██║ ██║██║███████╗██║ ██║██║ ██║██████╔╝ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║[/]
|
|
|
|
|
|
[#7A3511] ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝[/]""",
|
2026-03-10 03:54:12 -07:00
|
|
|
|
"banner_hero": """[#FFD39A]⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⠶⠶⠶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#F29C38]⠀⠀⠀⠀⠀⠀⣴⠟⠁⠀⠀⠀⠀⠈⠻⣦⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#F29C38]⠀⠀⠀⠀⠀⣼⠏⠀⠀⠀✦⠀⠀⠀⠀⠹⣧⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#E2832B]⠀⠀⠀⠀⢰⡟⠀⠀⣀⣤⣤⣤⣀⠀⠀⠀⢻⡆⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#E2832B]⠀⠀⣠⡾⠛⠁⣠⣾⠟⠉⠀⠉⠻⣷⣄⠀⠈⠛⢷⣄⠀⠀[/]
|
|
|
|
|
|
[#C75B1D]⠀⣼⠟⠀⢀⣾⠟⠁⠀⠀⠀⠀⠀⠈⠻⣷⡀⠀⠻⣧⠀[/]
|
|
|
|
|
|
[#C75B1D]⢸⡟⠀⠀⣿⡟⠀⠀⠀🔥⠀⠀⠀⠀⢻⣿⠀⠀⢻⡇[/]
|
|
|
|
|
|
[#7A3511]⠀⠻⣦⡀⠘⢿⣧⡀⠀⠀⠀⠀⠀⢀⣼⡿⠃⢀⣴⠟⠀[/]
|
|
|
|
|
|
[#7A3511]⠀⠀⠈⠻⣦⣀⠙⢿⣷⣤⣤⣤⣾⡿⠋⣀⣴⠟⠁⠀⠀[/]
|
|
|
|
|
|
[#C75B1D]⠀⠀⠀⠀⠈⠙⠛⠶⠤⠭⠭⠤⠶⠛⠋⠁⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#F29C38]⠀⠀⠀⠀⠀⠀⠀⠀⣰⡿⢿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[#F29C38]⠀⠀⠀⠀⠀⠀⠀⣼⡟⠀⠀⢻⣧⠀⠀⠀⠀⠀⠀⠀⠀[/]
|
|
|
|
|
|
[dim #7A3511]⠀⠀⠀⠀⠀⠀⠀tail flame lit⠀⠀⠀⠀⠀⠀⠀⠀[/]""",
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
},
|
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 loading and management
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
_active_skin: Optional[SkinConfig] = None
|
|
|
|
|
|
_active_skin_name: str = "default"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _skins_dir() -> Path:
|
|
|
|
|
|
"""User skins directory."""
|
|
|
|
|
|
home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
|
|
|
|
return home / "skins"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_skin_from_yaml(path: Path) -> Optional[Dict[str, Any]]:
|
|
|
|
|
|
"""Load a skin definition from a YAML file."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
data = yaml.safe_load(f)
|
|
|
|
|
|
if isinstance(data, dict) and "name" in data:
|
|
|
|
|
|
return data
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Failed to load skin from %s: %s", path, e)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
|
|
|
|
|
"""Build a SkinConfig from a raw dict (built-in or loaded from YAML)."""
|
|
|
|
|
|
# Start with default values as base for missing keys
|
|
|
|
|
|
default = _BUILTIN_SKINS["default"]
|
|
|
|
|
|
colors = dict(default.get("colors", {}))
|
|
|
|
|
|
colors.update(data.get("colors", {}))
|
|
|
|
|
|
spinner = dict(default.get("spinner", {}))
|
|
|
|
|
|
spinner.update(data.get("spinner", {}))
|
|
|
|
|
|
branding = dict(default.get("branding", {}))
|
|
|
|
|
|
branding.update(data.get("branding", {}))
|
|
|
|
|
|
|
|
|
|
|
|
return SkinConfig(
|
|
|
|
|
|
name=data.get("name", "unknown"),
|
|
|
|
|
|
description=data.get("description", ""),
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
spinner=spinner,
|
|
|
|
|
|
branding=branding,
|
|
|
|
|
|
tool_prefix=data.get("tool_prefix", default.get("tool_prefix", "┊")),
|
feat: add poseidon/sisyphus/charizard skins + banner logo support
Adds 3 new built-in skins (poseidon, sisyphus, charizard) with full
customization — colors, spinner faces/verbs/wings, branding text, and
custom ASCII art banner logos. Total: 7 built-in skins.
Also adds banner_logo and banner_hero fields to SkinConfig, allowing
any skin to replace the HERMES-AGENT ASCII art logo and the caduceus
hero art with custom artwork. The CLI now renders the skin's logo when
available, falling back to the default Hermes logo.
Skins with custom logos: ares, poseidon, sisyphus, charizard
Skins using default logo: default, mono, slate
2026-03-10 02:11:50 -07:00
|
|
|
|
banner_logo=data.get("banner_logo", ""),
|
|
|
|
|
|
banner_hero=data.get("banner_hero", ""),
|
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
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_skins() -> List[Dict[str, str]]:
|
|
|
|
|
|
"""List all available skins (built-in + user-installed).
|
|
|
|
|
|
|
|
|
|
|
|
Returns list of {"name": ..., "description": ..., "source": "builtin"|"user"}.
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for name, data in _BUILTIN_SKINS.items():
|
|
|
|
|
|
result.append({
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"description": data.get("description", ""),
|
|
|
|
|
|
"source": "builtin",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
skins_path = _skins_dir()
|
|
|
|
|
|
if skins_path.is_dir():
|
|
|
|
|
|
for f in sorted(skins_path.glob("*.yaml")):
|
|
|
|
|
|
data = _load_skin_from_yaml(f)
|
|
|
|
|
|
if data:
|
|
|
|
|
|
skin_name = data.get("name", f.stem)
|
|
|
|
|
|
# Skip if it shadows a built-in
|
|
|
|
|
|
if any(s["name"] == skin_name for s in result):
|
|
|
|
|
|
continue
|
|
|
|
|
|
result.append({
|
|
|
|
|
|
"name": skin_name,
|
|
|
|
|
|
"description": data.get("description", ""),
|
|
|
|
|
|
"source": "user",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_skin(name: str) -> SkinConfig:
|
|
|
|
|
|
"""Load a skin by name. Checks user skins first, then built-in."""
|
|
|
|
|
|
# Check user skins directory
|
|
|
|
|
|
skins_path = _skins_dir()
|
|
|
|
|
|
user_file = skins_path / f"{name}.yaml"
|
|
|
|
|
|
if user_file.is_file():
|
|
|
|
|
|
data = _load_skin_from_yaml(user_file)
|
|
|
|
|
|
if data:
|
|
|
|
|
|
return _build_skin_config(data)
|
|
|
|
|
|
|
|
|
|
|
|
# Check built-in skins
|
|
|
|
|
|
if name in _BUILTIN_SKINS:
|
|
|
|
|
|
return _build_skin_config(_BUILTIN_SKINS[name])
|
|
|
|
|
|
|
|
|
|
|
|
# Fallback to default
|
|
|
|
|
|
logger.warning("Skin '%s' not found, using default", name)
|
|
|
|
|
|
return _build_skin_config(_BUILTIN_SKINS["default"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_active_skin() -> SkinConfig:
|
|
|
|
|
|
"""Get the currently active skin config (cached)."""
|
|
|
|
|
|
global _active_skin
|
|
|
|
|
|
if _active_skin is None:
|
|
|
|
|
|
_active_skin = load_skin(_active_skin_name)
|
|
|
|
|
|
return _active_skin
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_active_skin(name: str) -> SkinConfig:
|
|
|
|
|
|
"""Switch the active skin. Returns the new SkinConfig."""
|
|
|
|
|
|
global _active_skin, _active_skin_name
|
|
|
|
|
|
_active_skin_name = name
|
|
|
|
|
|
_active_skin = load_skin(name)
|
|
|
|
|
|
return _active_skin
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_active_skin_name() -> str:
|
|
|
|
|
|
"""Get the name of the currently active skin."""
|
|
|
|
|
|
return _active_skin_name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_skin_from_config(config: dict) -> None:
|
|
|
|
|
|
"""Initialize the active skin from CLI config at startup.
|
|
|
|
|
|
|
|
|
|
|
|
Call this once during CLI init with the loaded config dict.
|
|
|
|
|
|
"""
|
|
|
|
|
|
display = config.get("display", {})
|
|
|
|
|
|
skin_name = display.get("skin", "default")
|
|
|
|
|
|
if isinstance(skin_name, str) and skin_name.strip():
|
|
|
|
|
|
set_active_skin(skin_name.strip())
|
|
|
|
|
|
else:
|
|
|
|
|
|
set_active_skin("default")
|