refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)

Centralizes two widely-duplicated patterns into hermes_constants.py:

1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
   - Was copy-pasted inline across 30+ files as:
     Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
   - Now defined once in hermes_constants.py (zero-dependency module)
   - hermes_cli/config.py re-exports it for backward compatibility
   - Removed local wrapper functions in honcho_integration/client.py,
     tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py

2. parse_reasoning_effort() — Reasoning effort string validation
   - Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
   - Same validation logic: check against (xhigh, high, medium, low, minimal, none)
   - Now defined once in hermes_constants.py, called from all 3 locations
   - Warning log for unknown values kept at call sites (context-specific)

31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
This commit is contained in:
Teknium 2026-03-25 15:54:28 -07:00 committed by GitHub
parent e0cfc089da
commit 77bcaba2d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 125 additions and 94 deletions

View file

@ -18,6 +18,7 @@ import logging
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
def _setup_logging() -> None: def _setup_logging() -> None:
@ -44,7 +45,7 @@ def _load_env() -> None:
"""Load .env from HERMES_HOME (default ``~/.hermes``).""" """Load .env from HERMES_HOME (default ``~/.hermes``)."""
from hermes_cli.env_loader import load_hermes_dotenv from hermes_cli.env_loader import load_hermes_dotenv
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) hermes_home = get_hermes_home()
loaded = load_hermes_dotenv(hermes_home=hermes_home) loaded = load_hermes_dotenv(hermes_home=hermes_home)
if loaded: if loaded:
for env_file in loaded: for env_file in loaded:

View file

@ -8,6 +8,8 @@ history.
""" """
from __future__ import annotations from __future__ import annotations
from hermes_constants import get_hermes_home
import copy import copy
import json import json
import logging import logging
@ -251,7 +253,7 @@ class SessionManager:
import os import os
from pathlib import Path from pathlib import Path
from hermes_state import SessionDB from hermes_state import SessionDB
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) hermes_home = get_hermes_home()
self._db_instance = SessionDB(db_path=hermes_home / "state.db") self._db_instance = SessionDB(db_path=hermes_home / "state.db")
return self._db_instance return self._db_instance
except Exception: except Exception:

View file

@ -14,6 +14,8 @@ import json
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -450,7 +452,7 @@ _OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
_OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token" _OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
_OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback" _OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
_OAUTH_SCOPES = "org:create_api_key user:profile user:inference" _OAUTH_SCOPES = "org:create_api_key user:profile user:inference"
_HERMES_OAUTH_FILE = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))) / ".anthropic_oauth.json" _HERMES_OAUTH_FILE = get_hermes_home() / ".anthropic_oauth.json"
def _generate_pkce() -> tuple: def _generate_pkce() -> tuple:

View file

@ -8,6 +8,8 @@ import logging
import os import os
import re import re
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Optional from typing import Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -320,7 +322,7 @@ def build_skills_system_prompt(
match skills by meaning, not just name. match skills by meaning, not just name.
Filters out skills incompatible with the current OS platform. Filters out skills incompatible with the current OS platform.
""" """
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) hermes_home = get_hermes_home()
skills_dir = hermes_home / "skills" skills_dir = hermes_home / "skills"
if not skills_dir.exists(): if not skills_dir.exists():
@ -449,7 +451,7 @@ def load_soul_md() -> Optional[str]:
except Exception as e: except Exception as e:
logger.debug("Could not ensure HERMES_HOME before loading SOUL.md: %s", e) logger.debug("Could not ensure HERMES_HOME before loading SOUL.md: %s", e)
soul_path = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "SOUL.md" soul_path = get_hermes_home() / "SOUL.md"
if not soul_path.exists(): if not soul_path.exists():
return None return None
try: try:

27
cli.py
View file

@ -70,10 +70,10 @@ _COMMAND_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧
# Load .env from ~/.hermes/.env first, then project root as dev fallback. # Load .env from ~/.hermes/.env first, then project root as dev fallback.
# User-managed env files should override stale shell exports on restart. # User-managed env files should override stale shell exports on restart.
from hermes_constants import OPENROUTER_BASE_URL from hermes_constants import get_hermes_home, OPENROUTER_BASE_URL
from hermes_cli.env_loader import load_hermes_dotenv from hermes_cli.env_loader import load_hermes_dotenv
_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) _hermes_home = get_hermes_home()
_project_env = Path(__file__).parent / '.env' _project_env = Path(__file__).parent / '.env'
load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env) load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env)
@ -112,21 +112,12 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:
def _parse_reasoning_config(effort: str) -> dict | None: def _parse_reasoning_config(effort: str) -> dict | None:
"""Parse a reasoning effort level into an OpenRouter reasoning config dict. """Parse a reasoning effort level into an OpenRouter reasoning config dict."""
from hermes_constants import parse_reasoning_effort
Valid levels: "xhigh", "high", "medium", "low", "minimal", "none". result = parse_reasoning_effort(effort)
Returns None to use the default (medium), or a config dict to override. if effort and effort.strip() and result is None:
""" logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort)
if not effort or not effort.strip(): return result
return None
effort = effort.strip().lower()
if effort == "none":
return {"enabled": False}
valid = ("xhigh", "high", "medium", "low", "minimal")
if effort in valid:
return {"enabled": True, "effort": effort}
logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort)
return None
def load_cli_config() -> Dict[str, Any]: def load_cli_config() -> Dict[str, Any]:
@ -2316,7 +2307,7 @@ class HermesCLI:
""" """
from hermes_cli.clipboard import save_clipboard_image from hermes_cli.clipboard import save_clipboard_image
img_dir = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "images" img_dir = get_hermes_home() / "images"
self._image_counter += 1 self._image_counter += 1
ts = datetime.now().strftime("%Y%m%d_%H%M%S") ts = datetime.now().strftime("%Y%m%d_%H%M%S")
img_path = img_dir / f"clip_{ts}_{self._image_counter}.png" img_path = img_dir / f"clip_{ts}_{self._image_counter}.png"

View file

@ -14,6 +14,7 @@ import re
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,7 +31,7 @@ except ImportError:
# Configuration # Configuration
# ============================================================================= # =============================================================================
HERMES_DIR = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) HERMES_DIR = get_hermes_home()
CRON_DIR = HERMES_DIR / "cron" CRON_DIR = HERMES_DIR / "cron"
JOBS_FILE = CRON_DIR / "jobs.json" JOBS_FILE = CRON_DIR / "jobs.json"
OUTPUT_DIR = CRON_DIR / "output" OUTPUT_DIR = CRON_DIR / "output"

View file

@ -25,6 +25,7 @@ except ImportError:
except ImportError: except ImportError:
msvcrt = None msvcrt = None
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Optional from typing import Optional
from hermes_time import now as _hermes_now from hermes_time import now as _hermes_now
@ -42,7 +43,7 @@ from cron.jobs import get_due_jobs, mark_job_run, save_job_output
SILENT_MARKER = "[SILENT]" SILENT_MARKER = "[SILENT]"
# Resolve Hermes home directory (respects HERMES_HOME override) # Resolve Hermes home directory (respects HERMES_HOME override)
_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) _hermes_home = get_hermes_home()
# File-based lock prevents concurrent ticks from gateway + daemon + systemd timer # File-based lock prevents concurrent ticks from gateway + daemon + systemd timer
_LOCK_DIR = _hermes_home / "cron" _LOCK_DIR = _hermes_home / "cron"
@ -327,16 +328,11 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
logger.warning("Job '%s': failed to load config.yaml, using defaults: %s", job_id, e) logger.warning("Job '%s': failed to load config.yaml, using defaults: %s", job_id, e)
# Reasoning config from env or config.yaml # Reasoning config from env or config.yaml
reasoning_config = None from hermes_constants import parse_reasoning_effort
effort = os.getenv("HERMES_REASONING_EFFORT", "") effort = os.getenv("HERMES_REASONING_EFFORT", "")
if not effort: if not effort:
effort = str(_cfg.get("agent", {}).get("reasoning_effort", "")).strip() effort = str(_cfg.get("agent", {}).get("reasoning_effort", "")).strip()
if effort and effort.lower() != "none": reasoning_config = parse_reasoning_effort(effort)
valid = ("xhigh", "high", "medium", "low", "minimal")
if effort.lower() in valid:
reasoning_config = {"enabled": True, "effort": effort.lower()}
elif effort.lower() == "none":
reasoning_config = {"enabled": False}
# Prefill messages from env or config.yaml # Prefill messages from env or config.yaml
prefill_messages = None prefill_messages = None

View file

@ -76,7 +76,8 @@ _ensure_ssl_certs()
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
# Resolve Hermes home directory (respects HERMES_HOME override) # Resolve Hermes home directory (respects HERMES_HOME override)
_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) from hermes_constants import get_hermes_home
_hermes_home = get_hermes_home()
# Load environment variables from ~/.hermes/.env first. # Load environment variables from ~/.hermes/.env first.
# User-managed env files should override stale shell exports on restart. # User-managed env files should override stale shell exports on restart.
@ -805,6 +806,7 @@ class GatewayRunner:
"medium", "low", "minimal", "none". Returns None to use default "medium", "low", "minimal", "none". Returns None to use default
(medium). (medium).
""" """
from hermes_constants import parse_reasoning_effort
effort = "" effort = ""
try: try:
import yaml as _y import yaml as _y
@ -817,16 +819,10 @@ class GatewayRunner:
pass pass
if not effort: if not effort:
effort = os.getenv("HERMES_REASONING_EFFORT", "") effort = os.getenv("HERMES_REASONING_EFFORT", "")
if not effort: result = parse_reasoning_effort(effort)
return None if effort and effort.strip() and result is None:
effort = effort.lower().strip() logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort)
if effort == "none": return result
return {"enabled": False}
valid = ("xhigh", "high", "medium", "low", "minimal")
if effort in valid:
return {"enabled": True, "effort": effort}
logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort)
return None
@staticmethod @staticmethod
def _load_show_reasoning() -> bool: def _load_show_reasoning() -> bool:
@ -5743,7 +5739,7 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool =
except Exception: except Exception:
pass pass
else: else:
hermes_home = os.getenv("HERMES_HOME", "~/.hermes") hermes_home = str(get_hermes_home())
logger.error( logger.error(
"Another gateway instance is already running (PID %d, HERMES_HOME=%s). " "Another gateway instance is already running (PID %d, HERMES_HOME=%s). "
"Use 'hermes gateway restart' to replace it, or 'hermes gateway stop' first.", "Use 'hermes gateway restart' to replace it, or 'hermes gateway stop' first.",

View file

@ -17,6 +17,7 @@ import os
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Any, Optional from typing import Any, Optional
_GATEWAY_KIND = "hermes-gateway" _GATEWAY_KIND = "hermes-gateway"
@ -26,7 +27,7 @@ _LOCKS_DIRNAME = "gateway-locks"
def _get_pid_path() -> Path: def _get_pid_path() -> Path:
"""Return the path to the gateway PID file, respecting HERMES_HOME.""" """Return the path to the gateway PID file, respecting HERMES_HOME."""
home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) home = get_hermes_home()
return home / "gateway.pid" return home / "gateway.pid"

View file

@ -11,6 +11,7 @@ import subprocess
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Dict, List, Optional from typing import Dict, List, Optional
from rich.console import Console from rich.console import Console
@ -136,7 +137,7 @@ def check_for_updates() -> Optional[int]:
``~/.hermes/.update_check``). Returns the number of commits behind, ``~/.hermes/.update_check``). Returns the number of commits behind,
or ``None`` if the check fails or isn't applicable. or ``None`` if the check fails or isn't applicable.
""" """
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) hermes_home = get_hermes_home()
repo_dir = hermes_home / "hermes-agent" repo_dir = hermes_home / "hermes-agent"
cache_file = hermes_home / ".update_check" cache_file = hermes_home / ".update_check"

View file

@ -59,7 +59,7 @@ def is_managed() -> bool:
""" """
if os.getenv("HERMES_MANAGED", "").lower() in ("true", "1", "yes"): if os.getenv("HERMES_MANAGED", "").lower() in ("true", "1", "yes"):
return True return True
managed_marker = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))) / ".managed" managed_marker = get_hermes_home() / ".managed"
return managed_marker.exists() return managed_marker.exists()
def managed_error(action: str = "modify configuration"): def managed_error(action: str = "modify configuration"):
@ -76,9 +76,8 @@ def managed_error(action: str = "modify configuration"):
# Config paths # Config paths
# ============================================================================= # =============================================================================
def get_hermes_home() -> Path: # Re-export from hermes_constants — canonical definition lives there.
"""Get the Hermes home directory (~/.hermes).""" from hermes_constants import get_hermes_home # noqa: F811,E402
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
def get_config_path() -> Path: def get_config_path() -> Path:
"""Get the main config file path.""" """Get the main config file path."""

View file

@ -134,7 +134,7 @@ def get_service_name() -> str:
""" """
import hashlib import hashlib
from pathlib import Path as _Path # local import to avoid monkeypatch interference from pathlib import Path as _Path # local import to avoid monkeypatch interference
home = _Path(os.getenv("HERMES_HOME", _Path.home() / ".hermes")).resolve() home = get_hermes_home().resolve()
default = (_Path.home() / ".hermes").resolve() default = (_Path.home() / ".hermes").resolve()
if home == default: if home == default:
return _SERVICE_BASE return _SERVICE_BASE
@ -437,7 +437,7 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
path_entries.extend(["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]) path_entries.extend(["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"])
sane_path = ":".join(path_entries) sane_path = ":".join(path_entries)
hermes_home = str(Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")).resolve()) hermes_home = str(get_hermes_home().resolve())
if system: if system:
username, group_name, home_dir = _system_service_identity(run_as_user) username, group_name, home_dir = _system_service_identity(run_as_user)

View file

@ -101,6 +101,8 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -513,8 +515,7 @@ _active_skin_name: str = "default"
def _skins_dir() -> Path: def _skins_dir() -> Path:
"""User skins directory.""" """User skins directory."""
home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) return get_hermes_home() / "skins"
return home / "skins"
def _load_skin_from_yaml(path: Path) -> Optional[Dict[str, Any]]: def _load_skin_from_yaml(path: Path) -> Optional[Dict[str, Any]]:

View file

@ -11,6 +11,8 @@ import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from hermes_cli.colors import Colors, color from hermes_cli.colors import Colors, color
def log_info(msg: str): def log_info(msg: str):
@ -31,11 +33,6 @@ def get_project_root() -> Path:
return Path(__file__).parent.parent.resolve() return Path(__file__).parent.parent.resolve()
def get_hermes_home() -> Path:
"""Get the Hermes home directory (~/.hermes)."""
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
def find_shell_configs() -> list: def find_shell_configs() -> list:
"""Find shell configuration files that might have PATH entries.""" """Find shell configuration files that might have PATH entries."""
home = Path.home() home = Path.home()

View file

@ -4,6 +4,40 @@ Import-safe module with no dependencies — can be imported from anywhere
without risk of circular imports. without risk of circular imports.
""" """
import os
from pathlib import Path
def get_hermes_home() -> Path:
"""Return the Hermes home directory (default: ~/.hermes).
Reads HERMES_HOME env var, falls back to ~/.hermes.
This is the single source of truth all other copies should import this.
"""
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
VALID_REASONING_EFFORTS = ("xhigh", "high", "medium", "low", "minimal")
def parse_reasoning_effort(effort: str) -> dict | None:
"""Parse a reasoning effort level into a config dict.
Valid levels: "xhigh", "high", "medium", "low", "minimal", "none".
Returns None when the input is empty or unrecognized (caller uses default).
Returns {"enabled": False} for "none".
Returns {"enabled": True, "effort": <level>} for valid effort levels.
"""
if not effort or not effort.strip():
return None
effort = effort.strip().lower()
if effort == "none":
return {"enabled": False}
if effort in VALID_REASONING_EFFORTS:
return {"enabled": True, "effort": effort}
return None
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
OPENROUTER_MODELS_URL = f"{OPENROUTER_BASE_URL}/models" OPENROUTER_MODELS_URL = f"{OPENROUTER_BASE_URL}/models"
OPENROUTER_CHAT_URL = f"{OPENROUTER_BASE_URL}/chat/completions" OPENROUTER_CHAT_URL = f"{OPENROUTER_BASE_URL}/chat/completions"

View file

@ -21,10 +21,11 @@ import sqlite3
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
DEFAULT_DB_PATH = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "state.db" DEFAULT_DB_PATH = get_hermes_home() / "state.db"
SCHEMA_VERSION = 6 SCHEMA_VERSION = 6

View file

@ -17,6 +17,7 @@ import logging
import os import os
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Optional from typing import Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -48,7 +49,7 @@ def _resolve_timezone_name() -> str:
# 2. config.yaml ``timezone`` key # 2. config.yaml ``timezone`` key
try: try:
import yaml import yaml
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) hermes_home = get_hermes_home()
config_path = hermes_home / "config.yaml" config_path = hermes_home / "config.yaml"
if config_path.exists(): if config_path.exists():
with open(config_path) as f: with open(config_path) as f:

View file

@ -18,6 +18,8 @@ import os
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@ -29,11 +31,6 @@ GLOBAL_CONFIG_PATH = Path.home() / ".honcho" / "config.json"
HOST = "hermes" HOST = "hermes"
def _get_hermes_home() -> Path:
"""Get HERMES_HOME without importing hermes_cli (avoids circular deps)."""
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
def resolve_config_path() -> Path: def resolve_config_path() -> Path:
"""Return the active Honcho config path. """Return the active Honcho config path.
@ -41,7 +38,7 @@ def resolve_config_path() -> Path:
to ~/.honcho/config.json (global). Returns the global path if neither to ~/.honcho/config.json (global). Returns the global path if neither
exists (for first-time setup writes). exists (for first-time setup writes).
""" """
local_path = _get_hermes_home() / "honcho.json" local_path = get_hermes_home() / "honcho.json"
if local_path.exists(): if local_path.exists():
return local_path return local_path
return GLOBAL_CONFIG_PATH return GLOBAL_CONFIG_PATH

View file

@ -29,7 +29,7 @@ import yaml
# Load .env from ~/.hermes/.env first, then project root as dev fallback. # Load .env from ~/.hermes/.env first, then project root as dev fallback.
# User-managed env files should override stale shell exports on restart. # User-managed env files should override stale shell exports on restart.
_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) _hermes_home = get_hermes_home()
_project_env = Path(__file__).parent / '.env' _project_env = Path(__file__).parent / '.env'
from hermes_cli.env_loader import load_hermes_dotenv from hermes_cli.env_loader import load_hermes_dotenv
@ -60,7 +60,7 @@ from tools.rl_training_tool import get_missing_keys
# Config Loading # Config Loading
# ============================================================================ # ============================================================================
from hermes_constants import OPENROUTER_BASE_URL from hermes_constants import get_hermes_home, OPENROUTER_BASE_URL
DEFAULT_MODEL = "anthropic/claude-opus-4.5" DEFAULT_MODEL = "anthropic/claude-opus-4.5"
DEFAULT_BASE_URL = OPENROUTER_BASE_URL DEFAULT_BASE_URL = OPENROUTER_BASE_URL

View file

@ -45,11 +45,13 @@ import fire
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
# Load .env from ~/.hermes/.env first, then project root as dev fallback. # Load .env from ~/.hermes/.env first, then project root as dev fallback.
# User-managed env files should override stale shell exports on restart. # User-managed env files should override stale shell exports on restart.
from hermes_cli.env_loader import load_hermes_dotenv from hermes_cli.env_loader import load_hermes_dotenv
_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) _hermes_home = get_hermes_home()
_project_env = Path(__file__).parent / '.env' _project_env = Path(__file__).parent / '.env'
_loaded_env_paths = load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env) _loaded_env_paths = load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env)
if _loaded_env_paths: if _loaded_env_paths:
@ -855,7 +857,7 @@ class AIAgent:
self.session_id = f"{timestamp_str}_{short_uuid}" self.session_id = f"{timestamp_str}_{short_uuid}"
# Session logs go into ~/.hermes/sessions/ alongside gateway sessions # Session logs go into ~/.hermes/sessions/ alongside gateway sessions
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) hermes_home = get_hermes_home()
self.logs_dir = hermes_home / "sessions" self.logs_dir = hermes_home / "sessions"
self.logs_dir.mkdir(parents=True, exist_ok=True) self.logs_dir.mkdir(parents=True, exist_ok=True)
self.session_log_file = self.logs_dir / f"session_{self.session_id}.json" self.session_log_file = self.logs_dir / f"session_{self.session_id}.json"

View file

@ -24,6 +24,7 @@ import os
import shutil import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
# Constants # Constants
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CHECKPOINT_BASE = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "checkpoints" CHECKPOINT_BASE = get_hermes_home() / "checkpoints"
DEFAULT_EXCLUDES = [ DEFAULT_EXCLUDES = [
"node_modules/", "node_modules/",

View file

@ -31,12 +31,13 @@ import re
import tempfile import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Where memory files live # Where memory files live
MEMORY_DIR = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "memories" MEMORY_DIR = get_hermes_home() / "memories"
ENTRY_DELIMITER = "\n§\n" ENTRY_DELIMITER = "\n§\n"

View file

@ -44,6 +44,8 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ============================================================================ # ============================================================================
@ -55,7 +57,7 @@ HERMES_ROOT = Path(__file__).parent.parent
TINKER_ATROPOS_ROOT = HERMES_ROOT / "tinker-atropos" TINKER_ATROPOS_ROOT = HERMES_ROOT / "tinker-atropos"
ENVIRONMENTS_DIR = TINKER_ATROPOS_ROOT / "tinker_atropos" / "environments" ENVIRONMENTS_DIR = TINKER_ATROPOS_ROOT / "tinker_atropos" / "environments"
CONFIGS_DIR = TINKER_ATROPOS_ROOT / "configs" CONFIGS_DIR = TINKER_ATROPOS_ROOT / "configs"
LOGS_DIR = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "logs" / "rl_training" LOGS_DIR = get_hermes_home() / "logs" / "rl_training"
def _ensure_logs_dir(): def _ensure_logs_dir():
"""Lazily create logs directory on first use (avoid side effects at import time).""" """Lazily create logs directory on first use (avoid side effects at import time)."""

View file

@ -39,6 +39,7 @@ import re
import shutil import shutil
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -76,7 +77,7 @@ import yaml
# All skills live in ~/.hermes/skills/ (single source of truth) # All skills live in ~/.hermes/skills/ (single source of truth)
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) HERMES_HOME = get_hermes_home()
SKILLS_DIR = HERMES_HOME / "skills" SKILLS_DIR = HERMES_HOME / "skills"
MAX_NAME_LENGTH = 64 MAX_NAME_LENGTH = 64

View file

@ -25,6 +25,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
@ -42,7 +43,7 @@ logger = logging.getLogger(__name__)
# Paths # Paths
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) HERMES_HOME = get_hermes_home()
SKILLS_DIR = HERMES_HOME / "skills" SKILLS_DIR = HERMES_HOME / "skills"
HUB_DIR = SKILLS_DIR / ".hub" HUB_DIR = SKILLS_DIR / ".hub"
LOCK_FILE = HUB_DIR / "lock.json" LOCK_FILE = HUB_DIR / "lock.json"

View file

@ -26,12 +26,13 @@ import logging
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) HERMES_HOME = get_hermes_home()
SKILLS_DIR = HERMES_HOME / "skills" SKILLS_DIR = HERMES_HOME / "skills"
MANIFEST_FILE = SKILLS_DIR / ".bundled_manifest" MANIFEST_FILE = SKILLS_DIR / ".bundled_manifest"

View file

@ -68,6 +68,8 @@ Usage:
import json import json
import logging import logging
from hermes_constants import get_hermes_home
import os import os
import re import re
import sys import sys
@ -85,7 +87,7 @@ logger = logging.getLogger(__name__)
# All skills live in ~/.hermes/skills/ (seeded from bundled skills/ on install). # All skills live in ~/.hermes/skills/ (seeded from bundled skills/ on install).
# This is the single source of truth -- agent edits, hub installs, and bundled # This is the single source of truth -- agent edits, hub installs, and bundled
# skills all coexist here without polluting the git repo. # skills all coexist here without polluting the git repo.
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) HERMES_HOME = get_hermes_home()
SKILLS_DIR = HERMES_HOME / "skills" SKILLS_DIR = HERMES_HOME / "skills"
# Anthropic-recommended limits for progressive disclosure efficiency # Anthropic-recommended limits for progressive disclosure efficiency

View file

@ -34,6 +34,8 @@ import threading
import time import time
import urllib.request import urllib.request
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_REPO = "sheeki03/tirith" _REPO = "sheeki03/tirith"
@ -104,14 +106,8 @@ _MARKER_TTL = 86400 # 24 hours
def _get_hermes_home() -> str: def _get_hermes_home() -> str:
"""Return the Hermes home directory, respecting HERMES_HOME env var. """Return the Hermes home directory, respecting HERMES_HOME env var."""
return str(get_hermes_home())
Matches the convention used throughout the codebase (hermes_cli.config,
cli.py, gateway/run.py, etc.) so tirith state stays inside the active
profile and tests get automatic isolation via conftest's HERMES_HOME
monkeypatch.
"""
return os.getenv("HERMES_HOME") or os.path.join(os.path.expanduser("~"), ".hermes")
def _failure_marker_path() -> str: def _failure_marker_path() -> str:

View file

@ -32,6 +32,8 @@ import tempfile
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -83,7 +85,7 @@ def get_stt_model_from_config() -> Optional[str]:
""" """
try: try:
import yaml import yaml
cfg_path = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "config.yaml" cfg_path = get_hermes_home() / "config.yaml"
if cfg_path.exists(): if cfg_path.exists():
with open(cfg_path) as f: with open(cfg_path) as f:
data = yaml.safe_load(f) or {} data = yaml.safe_load(f) or {}

View file

@ -33,6 +33,7 @@ import subprocess
import tempfile import tempfile
import threading import threading
from pathlib import Path from pathlib import Path
from hermes_constants import get_hermes_home
from typing import Callable, Dict, Any, Optional from typing import Callable, Dict, Any, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -73,7 +74,7 @@ DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"
DEFAULT_ELEVENLABS_STREAMING_MODEL_ID = "eleven_flash_v2_5" DEFAULT_ELEVENLABS_STREAMING_MODEL_ID = "eleven_flash_v2_5"
DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts" DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts"
DEFAULT_OPENAI_VOICE = "alloy" DEFAULT_OPENAI_VOICE = "alloy"
DEFAULT_OUTPUT_DIR = str(Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "audio_cache") DEFAULT_OUTPUT_DIR = str(get_hermes_home() / "audio_cache")
MAX_TEXT_LENGTH = 4000 MAX_TEXT_LENGTH = 4000

View file

@ -19,6 +19,8 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_DEFAULT_WEBSITE_BLOCKLIST = { _DEFAULT_WEBSITE_BLOCKLIST = {
@ -36,12 +38,8 @@ _cached_policy_path: Optional[str] = None
_cached_policy_time: float = 0.0 _cached_policy_time: float = 0.0
def _get_hermes_home() -> Path:
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
def _get_default_config_path() -> Path: def _get_default_config_path() -> Path:
return _get_hermes_home() / "config.yaml" return get_hermes_home() / "config.yaml"
class WebsitePolicyError(Exception): class WebsitePolicyError(Exception):
@ -182,7 +180,7 @@ def load_website_blocklist(config_path: Optional[Path] = None) -> Dict[str, Any]
continue continue
path = Path(shared_file).expanduser() path = Path(shared_file).expanduser()
if not path.is_absolute(): if not path.is_absolute():
path = (_get_hermes_home() / path).resolve() path = (get_hermes_home() / path).resolve()
for normalized in _iter_blocklist_file_rules(path): for normalized in _iter_blocklist_file_rules(path):
key = (str(path), normalized) key = (str(path), normalized)
if key in seen: if key in seen: