Hermes Agent UX Improvements
This commit is contained in:
parent
b1f55e3ee5
commit
ededaaa874
23 changed files with 945 additions and 1545 deletions
|
|
@ -11,4 +11,4 @@ Provides subcommands for:
|
|||
- hermes cron - Manage cron jobs
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "v1.0.0"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def cprint(text: str):
|
|||
# ASCII Art & Branding
|
||||
# =========================================================================
|
||||
|
||||
VERSION = "v1.0.0"
|
||||
from hermes_cli import __version__ as VERSION
|
||||
|
||||
HERMES_AGENT_LOGO = """[bold #FFD700]██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
||||
[bold #FFD700]██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ def run_doctor(args):
|
|||
should_fix = getattr(args, 'fix', False)
|
||||
|
||||
issues = []
|
||||
manual_issues = [] # issues that can't be auto-fixed
|
||||
fixed_count = 0
|
||||
|
||||
print()
|
||||
print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN))
|
||||
|
|
@ -135,8 +137,15 @@ def run_doctor(args):
|
|||
check_ok(".env file exists (in project directory)")
|
||||
else:
|
||||
check_fail("~/.hermes/.env file missing")
|
||||
check_info("Run 'hermes setup' to create one")
|
||||
issues.append("Run 'hermes setup' to create .env")
|
||||
if should_fix:
|
||||
env_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
env_path.touch()
|
||||
check_ok("Created empty ~/.hermes/.env")
|
||||
check_info("Run 'hermes setup' to configure API keys")
|
||||
fixed_count += 1
|
||||
else:
|
||||
check_info("Run 'hermes setup' to create one")
|
||||
issues.append("Run 'hermes setup' to create .env")
|
||||
|
||||
# Check ~/.hermes/config.yaml (primary) or project cli-config.yaml (fallback)
|
||||
config_path = HERMES_HOME / 'config.yaml'
|
||||
|
|
@ -147,7 +156,17 @@ def run_doctor(args):
|
|||
if fallback_config.exists():
|
||||
check_ok("cli-config.yaml exists (in project directory)")
|
||||
else:
|
||||
check_warn("config.yaml not found", "(using defaults)")
|
||||
example_config = PROJECT_ROOT / 'cli-config.yaml.example'
|
||||
if should_fix and example_config.exists():
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(example_config), str(config_path))
|
||||
check_ok("Created ~/.hermes/config.yaml from cli-config.yaml.example")
|
||||
fixed_count += 1
|
||||
elif should_fix:
|
||||
check_warn("config.yaml not found and no example to copy from")
|
||||
manual_issues.append("Create ~/.hermes/config.yaml manually")
|
||||
else:
|
||||
check_warn("config.yaml not found", "(using defaults)")
|
||||
|
||||
# =========================================================================
|
||||
# Check: Directory structure
|
||||
|
|
@ -159,7 +178,26 @@ def run_doctor(args):
|
|||
if hermes_home.exists():
|
||||
check_ok("~/.hermes directory exists")
|
||||
else:
|
||||
check_warn("~/.hermes not found", "(will be created on first use)")
|
||||
if should_fix:
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
check_ok("Created ~/.hermes directory")
|
||||
fixed_count += 1
|
||||
else:
|
||||
check_warn("~/.hermes not found", "(will be created on first use)")
|
||||
|
||||
# Check expected subdirectories
|
||||
expected_subdirs = ["cron", "sessions", "logs", "skills", "memories"]
|
||||
for subdir_name in expected_subdirs:
|
||||
subdir_path = hermes_home / subdir_name
|
||||
if subdir_path.exists():
|
||||
check_ok(f"~/.hermes/{subdir_name}/ exists")
|
||||
else:
|
||||
if should_fix:
|
||||
subdir_path.mkdir(parents=True, exist_ok=True)
|
||||
check_ok(f"Created ~/.hermes/{subdir_name}/")
|
||||
fixed_count += 1
|
||||
else:
|
||||
check_warn(f"~/.hermes/{subdir_name}/ not found", "(will be created on first use)")
|
||||
|
||||
# Check for SOUL.md persona file
|
||||
soul_path = hermes_home / "SOUL.md"
|
||||
|
|
@ -175,14 +213,25 @@ def run_doctor(args):
|
|||
check_warn("~/.hermes/SOUL.md not found", "(create it to give Hermes a custom personality)")
|
||||
if should_fix:
|
||||
soul_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
soul_path.write_text("# Hermes Agent Persona\n\n<!-- Edit this file to customize how Hermes communicates. -->\n", encoding="utf-8")
|
||||
check_ok("Created ~/.hermes/SOUL.md")
|
||||
soul_path.write_text(
|
||||
"# Hermes Agent Persona\n\n"
|
||||
"<!-- Edit this file to customize how Hermes communicates. -->\n\n"
|
||||
"You are Hermes, a helpful AI assistant.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
check_ok("Created ~/.hermes/SOUL.md with basic template")
|
||||
fixed_count += 1
|
||||
|
||||
logs_dir = PROJECT_ROOT / "logs"
|
||||
if logs_dir.exists():
|
||||
check_ok("logs/ directory exists")
|
||||
check_ok("logs/ directory exists (project root)")
|
||||
else:
|
||||
check_warn("logs/ not found", "(will be created on first use)")
|
||||
if should_fix:
|
||||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
check_ok("Created logs/ directory")
|
||||
fixed_count += 1
|
||||
else:
|
||||
check_warn("logs/ not found", "(will be created on first use)")
|
||||
|
||||
# Check memory directory
|
||||
memories_dir = hermes_home / "memories"
|
||||
|
|
@ -205,6 +254,7 @@ def run_doctor(args):
|
|||
if should_fix:
|
||||
memories_dir.mkdir(parents=True, exist_ok=True)
|
||||
check_ok("Created ~/.hermes/memories/")
|
||||
fixed_count += 1
|
||||
|
||||
# Check SQLite session store
|
||||
state_db_path = hermes_home / "state.db"
|
||||
|
|
@ -299,6 +349,7 @@ def run_doctor(args):
|
|||
|
||||
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
||||
if openrouter_key:
|
||||
print(" Checking OpenRouter API...", end="", flush=True)
|
||||
try:
|
||||
import httpx
|
||||
response = httpx.get(
|
||||
|
|
@ -307,20 +358,21 @@ def run_doctor(args):
|
|||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
check_ok("OpenRouter API")
|
||||
print(f"\r {color('✓', Colors.GREEN)} OpenRouter API ")
|
||||
elif response.status_code == 401:
|
||||
check_fail("OpenRouter API", "(invalid API key)")
|
||||
print(f"\r {color('✗', Colors.RED)} OpenRouter API {color('(invalid API key)', Colors.DIM)} ")
|
||||
issues.append("Check OPENROUTER_API_KEY in .env")
|
||||
else:
|
||||
check_fail("OpenRouter API", f"(HTTP {response.status_code})")
|
||||
print(f"\r {color('✗', Colors.RED)} OpenRouter API {color(f'(HTTP {response.status_code})', Colors.DIM)} ")
|
||||
except Exception as e:
|
||||
check_fail("OpenRouter API", f"({e})")
|
||||
print(f"\r {color('✗', Colors.RED)} OpenRouter API {color(f'({e})', Colors.DIM)} ")
|
||||
issues.append("Check network connectivity")
|
||||
else:
|
||||
check_warn("OpenRouter API", "(not configured)")
|
||||
|
||||
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
if anthropic_key:
|
||||
print(" Checking Anthropic API...", end="", flush=True)
|
||||
try:
|
||||
import httpx
|
||||
response = httpx.get(
|
||||
|
|
@ -332,14 +384,14 @@ def run_doctor(args):
|
|||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
check_ok("Anthropic API")
|
||||
print(f"\r {color('✓', Colors.GREEN)} Anthropic API ")
|
||||
elif response.status_code == 401:
|
||||
check_fail("Anthropic API", "(invalid API key)")
|
||||
print(f"\r {color('✗', Colors.RED)} Anthropic API {color('(invalid API key)', Colors.DIM)} ")
|
||||
else:
|
||||
# Note: Anthropic may not have /models endpoint
|
||||
check_warn("Anthropic API", "(couldn't verify)")
|
||||
msg = "(couldn't verify)"
|
||||
print(f"\r {color('⚠', Colors.YELLOW)} Anthropic API {color(msg, Colors.DIM)} ")
|
||||
except Exception as e:
|
||||
check_warn("Anthropic API", f"({e})")
|
||||
print(f"\r {color('⚠', Colors.YELLOW)} Anthropic API {color(f'({e})', Colors.DIM)} ")
|
||||
|
||||
# =========================================================================
|
||||
# Check: Submodules
|
||||
|
|
@ -440,17 +492,28 @@ def run_doctor(args):
|
|||
# Summary
|
||||
# =========================================================================
|
||||
print()
|
||||
if issues:
|
||||
print(color("─" * 60, Colors.YELLOW))
|
||||
print(color(f" Found {len(issues)} issue(s) to address:", Colors.YELLOW, Colors.BOLD))
|
||||
remaining_issues = issues + manual_issues
|
||||
if should_fix and fixed_count > 0:
|
||||
print(color("─" * 60, Colors.GREEN))
|
||||
print(color(f" Fixed {fixed_count} issue(s).", Colors.GREEN, Colors.BOLD), end="")
|
||||
if remaining_issues:
|
||||
print(color(f" {len(remaining_issues)} issue(s) require manual intervention.", Colors.YELLOW, Colors.BOLD))
|
||||
else:
|
||||
print()
|
||||
print()
|
||||
for i, issue in enumerate(issues, 1):
|
||||
if remaining_issues:
|
||||
for i, issue in enumerate(remaining_issues, 1):
|
||||
print(f" {i}. {issue}")
|
||||
print()
|
||||
elif remaining_issues:
|
||||
print(color("─" * 60, Colors.YELLOW))
|
||||
print(color(f" Found {len(remaining_issues)} issue(s) to address:", Colors.YELLOW, Colors.BOLD))
|
||||
print()
|
||||
for i, issue in enumerate(remaining_issues, 1):
|
||||
print(f" {i}. {issue}")
|
||||
print()
|
||||
|
||||
if should_fix:
|
||||
print(color(" Attempting auto-fix is not yet implemented.", Colors.DIM))
|
||||
print(color(" Please resolve issues manually.", Colors.DIM))
|
||||
if not should_fix:
|
||||
print(color(" Tip: run 'hermes doctor --fix' to auto-fix what's possible.", Colors.DIM))
|
||||
else:
|
||||
print(color("─" * 60, Colors.GREEN))
|
||||
print(color(" All checks passed! 🎉", Colors.GREEN, Colors.BOLD))
|
||||
|
|
|
|||
|
|
@ -47,8 +47,66 @@ from hermes_constants import OPENROUTER_BASE_URL
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _has_any_provider_configured() -> bool:
|
||||
"""Check if at least one inference provider is usable."""
|
||||
from hermes_cli.config import get_env_path, get_hermes_home
|
||||
|
||||
# Check env vars (may be set by .env or shell)
|
||||
if os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY"):
|
||||
return True
|
||||
|
||||
# Check .env file for keys
|
||||
env_file = get_env_path()
|
||||
if env_file.exists():
|
||||
try:
|
||||
for line in env_file.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, val = line.partition("=")
|
||||
val = val.strip().strip("'\"")
|
||||
if key.strip() in ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY") and val:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check for Nous Portal OAuth credentials
|
||||
auth_file = get_hermes_home() / "auth.json"
|
||||
if auth_file.exists():
|
||||
try:
|
||||
import json
|
||||
auth = json.loads(auth_file.read_text())
|
||||
active = auth.get("active_provider")
|
||||
if active:
|
||||
state = auth.get("providers", {}).get(active, {})
|
||||
if state.get("access_token") or state.get("refresh_token"):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def cmd_chat(args):
|
||||
"""Run interactive chat CLI."""
|
||||
# First-run guard: check if any provider is configured before launching
|
||||
if not _has_any_provider_configured():
|
||||
print()
|
||||
print("It looks like Hermes isn't configured yet -- no API keys or providers found.")
|
||||
print()
|
||||
print(" Run: hermes setup")
|
||||
print()
|
||||
try:
|
||||
reply = input("Run setup now? [Y/n] ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
reply = "n"
|
||||
if reply in ("", "y", "yes"):
|
||||
cmd_setup(args)
|
||||
return
|
||||
print()
|
||||
print("You can run 'hermes setup' at any time to configure.")
|
||||
sys.exit(1)
|
||||
|
||||
# Import and run the CLI
|
||||
from cli import main as cli_main
|
||||
|
||||
|
|
@ -219,20 +277,10 @@ def _model_flow_openrouter(config, current_model=""):
|
|||
print("API key saved.")
|
||||
print()
|
||||
|
||||
OPENROUTER_MODELS = [
|
||||
"anthropic/claude-opus-4.6",
|
||||
"anthropic/claude-sonnet-4.5",
|
||||
"anthropic/claude-opus-4.5",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.2-codex",
|
||||
"google/gemini-3-pro-preview",
|
||||
"google/gemini-3-flash-preview",
|
||||
"z-ai/glm-4.7",
|
||||
"moonshotai/kimi-k2.5",
|
||||
"minimax/minimax-m2.1",
|
||||
]
|
||||
from hermes_cli.models import model_ids
|
||||
openrouter_models = model_ids()
|
||||
|
||||
selected = _prompt_model_selection(OPENROUTER_MODELS, current_model=current_model)
|
||||
selected = _prompt_model_selection(openrouter_models, current_model=current_model)
|
||||
if selected:
|
||||
# Clear any custom endpoint and set provider to openrouter
|
||||
if get_env_value("OPENAI_BASE_URL"):
|
||||
|
|
|
|||
33
hermes_cli/models.py
Normal file
33
hermes_cli/models.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
Canonical list of OpenRouter models offered in CLI and setup wizards.
|
||||
|
||||
Add, remove, or reorder entries here — both `hermes setup` and
|
||||
`hermes` provider-selection will pick up the change automatically.
|
||||
"""
|
||||
|
||||
# (model_id, display description shown in menus)
|
||||
OPENROUTER_MODELS: list[tuple[str, str]] = [
|
||||
("anthropic/claude-opus-4.6", "recommended"),
|
||||
("anthropic/claude-sonnet-4.5", ""),
|
||||
("anthropic/claude-opus-4.5", ""),
|
||||
("openai/gpt-5.2", ""),
|
||||
("openai/gpt-5.2-codex", ""),
|
||||
("google/gemini-3-pro-preview", ""),
|
||||
("google/gemini-3-flash-preview", ""),
|
||||
("z-ai/glm-4.7", ""),
|
||||
("moonshotai/kimi-k2.5", ""),
|
||||
("minimax/minimax-m2.1", ""),
|
||||
]
|
||||
|
||||
|
||||
def model_ids() -> list[str]:
|
||||
"""Return just the model-id strings (convenience helper)."""
|
||||
return [mid for mid, _ in OPENROUTER_MODELS]
|
||||
|
||||
|
||||
def menu_labels() -> list[str]:
|
||||
"""Return display labels like 'anthropic/claude-opus-4.6 (recommended)'."""
|
||||
labels = []
|
||||
for mid, desc in OPENROUTER_MODELS:
|
||||
labels.append(f"{mid} ({desc})" if desc else mid)
|
||||
return labels
|
||||
|
|
@ -611,46 +611,27 @@ def run_setup_wizard(args):
|
|||
save_env_value("LLM_MODEL", custom)
|
||||
# else: keep current
|
||||
else:
|
||||
# Static list for OpenRouter / fallback
|
||||
model_choices = [
|
||||
"anthropic/claude-opus-4.6 (recommended)",
|
||||
"anthropic/claude-sonnet-4.5",
|
||||
"anthropic/claude-opus-4.5",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5.2-codex",
|
||||
"google/gemini-3-pro-preview",
|
||||
"google/gemini-3-flash-preview",
|
||||
"z-ai/glm-4.7",
|
||||
"moonshotai/kimi-k2.5",
|
||||
"minimax/minimax-m2.1",
|
||||
# Static list for OpenRouter / fallback (from canonical list)
|
||||
from hermes_cli.models import model_ids, menu_labels
|
||||
|
||||
ids = model_ids()
|
||||
model_choices = menu_labels() + [
|
||||
"Custom model",
|
||||
f"Keep current ({current_model})"
|
||||
f"Keep current ({current_model})",
|
||||
]
|
||||
|
||||
model_idx = prompt_choice("Select default model:", model_choices, 11)
|
||||
keep_idx = len(model_choices) - 1
|
||||
model_idx = prompt_choice("Select default model:", model_choices, keep_idx)
|
||||
|
||||
model_map = {
|
||||
0: "anthropic/claude-opus-4.6",
|
||||
1: "anthropic/claude-sonnet-4.5",
|
||||
2: "anthropic/claude-opus-4.5",
|
||||
3: "openai/gpt-5.2",
|
||||
4: "openai/gpt-5.2-codex",
|
||||
5: "google/gemini-3-pro-preview",
|
||||
6: "google/gemini-3-flash-preview",
|
||||
7: "z-ai/glm-4.7",
|
||||
8: "moonshotai/kimi-k2.5",
|
||||
9: "minimax/minimax-m2.1",
|
||||
}
|
||||
|
||||
if model_idx in model_map:
|
||||
config['model'] = model_map[model_idx]
|
||||
save_env_value("LLM_MODEL", model_map[model_idx])
|
||||
elif model_idx == 10: # Custom
|
||||
if model_idx < len(ids):
|
||||
config['model'] = ids[model_idx]
|
||||
save_env_value("LLM_MODEL", ids[model_idx])
|
||||
elif model_idx == len(ids): # Custom
|
||||
custom = prompt("Enter model name (e.g., anthropic/claude-opus-4.6)")
|
||||
if custom:
|
||||
config['model'] = custom
|
||||
save_env_value("LLM_MODEL", custom)
|
||||
# else: Keep current (model_idx == 11)
|
||||
# else: Keep current
|
||||
|
||||
# =========================================================================
|
||||
# Step 4: Terminal Backend
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue