From 77bcaba2d7e98fc7e28dcb6998086006d72fd667 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:54:28 -0700 Subject: [PATCH] refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- acp_adapter/entry.py | 3 ++- acp_adapter/session.py | 4 +++- agent/anthropic_adapter.py | 4 +++- agent/prompt_builder.py | 6 ++++-- cli.py | 27 +++++++++------------------ cron/jobs.py | 3 ++- cron/scheduler.py | 12 ++++-------- gateway/run.py | 20 ++++++++------------ gateway/status.py | 3 ++- hermes_cli/banner.py | 3 ++- hermes_cli/config.py | 7 +++---- hermes_cli/gateway.py | 4 ++-- hermes_cli/skin_engine.py | 5 +++-- hermes_cli/uninstall.py | 7 ++----- hermes_constants.py | 34 ++++++++++++++++++++++++++++++++++ hermes_state.py | 3 ++- hermes_time.py | 3 ++- honcho_integration/client.py | 9 +++------ rl_cli.py | 4 ++-- run_agent.py | 6 ++++-- tools/checkpoint_manager.py | 3 ++- tools/memory_tool.py | 3 ++- tools/rl_training_tool.py | 4 +++- tools/skill_manager_tool.py | 3 ++- tools/skills_hub.py | 3 ++- tools/skills_sync.py | 3 ++- tools/skills_tool.py | 4 +++- tools/tirith_security.py | 12 ++++-------- tools/transcription_tools.py | 4 +++- tools/tts_tool.py | 3 ++- tools/website_policy.py | 10 ++++------ 31 files changed, 125 insertions(+), 94 deletions(-) diff --git a/acp_adapter/entry.py b/acp_adapter/entry.py index 820e55f8..fe13ce70 100644 --- a/acp_adapter/entry.py +++ b/acp_adapter/entry.py @@ -18,6 +18,7 @@ import logging import os import sys from pathlib import Path +from hermes_constants import get_hermes_home def _setup_logging() -> None: @@ -44,7 +45,7 @@ def _load_env() -> None: """Load .env from HERMES_HOME (default ``~/.hermes``).""" 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) if loaded: for env_file in loaded: diff --git a/acp_adapter/session.py b/acp_adapter/session.py index 629b086f..c9069d1e 100644 --- a/acp_adapter/session.py +++ b/acp_adapter/session.py @@ -8,6 +8,8 @@ history. """ from __future__ import annotations +from hermes_constants import get_hermes_home + import copy import json import logging @@ -251,7 +253,7 @@ class SessionManager: import os from pathlib import Path 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") return self._db_instance except Exception: diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index fc5c460d..661663e0 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -14,6 +14,8 @@ import json import logging import os from pathlib import Path + +from hermes_constants import get_hermes_home from types import SimpleNamespace 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_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback" _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: diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 7fe8f3ce..640a2ddb 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -8,6 +8,8 @@ import logging import os import re from pathlib import Path + +from hermes_constants import get_hermes_home from typing import Optional logger = logging.getLogger(__name__) @@ -320,7 +322,7 @@ def build_skills_system_prompt( match skills by meaning, not just name. 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" if not skills_dir.exists(): @@ -449,7 +451,7 @@ def load_soul_md() -> Optional[str]: except Exception as 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(): return None try: diff --git a/cli.py b/cli.py index 2e374423..f8224579 100644 --- a/cli.py +++ b/cli.py @@ -70,10 +70,10 @@ _COMMAND_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧ # Load .env from ~/.hermes/.env first, then project root as dev fallback. # 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 -_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +_hermes_home = get_hermes_home() _project_env = Path(__file__).parent / '.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: - """Parse a reasoning effort level into an OpenRouter reasoning config dict. - - Valid levels: "xhigh", "high", "medium", "low", "minimal", "none". - Returns None to use the default (medium), or a config dict to override. - """ - if not effort or not effort.strip(): - 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 + """Parse a reasoning effort level into an OpenRouter reasoning config dict.""" + from hermes_constants import parse_reasoning_effort + result = parse_reasoning_effort(effort) + if effort and effort.strip() and result is None: + logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort) + return result def load_cli_config() -> Dict[str, Any]: @@ -2316,7 +2307,7 @@ class HermesCLI: """ 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 ts = datetime.now().strftime("%Y%m%d_%H%M%S") img_path = img_dir / f"clip_{ts}_{self._image_counter}.png" diff --git a/cron/jobs.py b/cron/jobs.py index 1dd6c680..841f5633 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -14,6 +14,7 @@ import re import uuid from datetime import datetime, timedelta from pathlib import Path +from hermes_constants import get_hermes_home from typing import Optional, Dict, List, Any logger = logging.getLogger(__name__) @@ -30,7 +31,7 @@ except ImportError: # Configuration # ============================================================================= -HERMES_DIR = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +HERMES_DIR = get_hermes_home() CRON_DIR = HERMES_DIR / "cron" JOBS_FILE = CRON_DIR / "jobs.json" OUTPUT_DIR = CRON_DIR / "output" diff --git a/cron/scheduler.py b/cron/scheduler.py index 9f92897f..133b4c1b 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -25,6 +25,7 @@ except ImportError: except ImportError: msvcrt = None from pathlib import Path +from hermes_constants import get_hermes_home from typing import Optional 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]" # 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 _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) # Reasoning config from env or config.yaml - reasoning_config = None + from hermes_constants import parse_reasoning_effort effort = os.getenv("HERMES_REASONING_EFFORT", "") if not effort: effort = str(_cfg.get("agent", {}).get("reasoning_effort", "")).strip() - if effort and effort.lower() != "none": - 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} + reasoning_config = parse_reasoning_effort(effort) # Prefill messages from env or config.yaml prefill_messages = None diff --git a/gateway/run.py b/gateway/run.py index ad97a1ba..0c0f003b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -76,7 +76,8 @@ _ensure_ssl_certs() sys.path.insert(0, str(Path(__file__).parent.parent)) # 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. # 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). """ + from hermes_constants import parse_reasoning_effort effort = "" try: import yaml as _y @@ -817,16 +819,10 @@ class GatewayRunner: pass if not effort: effort = os.getenv("HERMES_REASONING_EFFORT", "") - if not effort: - return None - effort = effort.lower().strip() - 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 + result = parse_reasoning_effort(effort) + if effort and effort.strip() and result is None: + logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort) + return result @staticmethod def _load_show_reasoning() -> bool: @@ -5743,7 +5739,7 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool = except Exception: pass else: - hermes_home = os.getenv("HERMES_HOME", "~/.hermes") + hermes_home = str(get_hermes_home()) logger.error( "Another gateway instance is already running (PID %d, HERMES_HOME=%s). " "Use 'hermes gateway restart' to replace it, or 'hermes gateway stop' first.", diff --git a/gateway/status.py b/gateway/status.py index f5f5649b..b0ea693a 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -17,6 +17,7 @@ import os import sys from datetime import datetime, timezone from pathlib import Path +from hermes_constants import get_hermes_home from typing import Any, Optional _GATEWAY_KIND = "hermes-gateway" @@ -26,7 +27,7 @@ _LOCKS_DIRNAME = "gateway-locks" def _get_pid_path() -> Path: """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" diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index 403c1876..c4eb827e 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -11,6 +11,7 @@ import subprocess import threading import time from pathlib import Path +from hermes_constants import get_hermes_home from typing import Dict, List, Optional from rich.console import Console @@ -136,7 +137,7 @@ def check_for_updates() -> Optional[int]: ``~/.hermes/.update_check``). Returns the number of commits behind, or ``None`` if the check fails or isn't applicable. """ - hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) + hermes_home = get_hermes_home() repo_dir = hermes_home / "hermes-agent" cache_file = hermes_home / ".update_check" diff --git a/hermes_cli/config.py b/hermes_cli/config.py index d73edc6a..826e3a8b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -59,7 +59,7 @@ def is_managed() -> bool: """ if os.getenv("HERMES_MANAGED", "").lower() in ("true", "1", "yes"): return True - managed_marker = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))) / ".managed" + managed_marker = get_hermes_home() / ".managed" return managed_marker.exists() def managed_error(action: str = "modify configuration"): @@ -76,9 +76,8 @@ def managed_error(action: str = "modify configuration"): # Config paths # ============================================================================= -def get_hermes_home() -> Path: - """Get the Hermes home directory (~/.hermes).""" - return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +# Re-export from hermes_constants — canonical definition lives there. +from hermes_constants import get_hermes_home # noqa: F811,E402 def get_config_path() -> Path: """Get the main config file path.""" diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index d1d79867..24affcdc 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -134,7 +134,7 @@ def get_service_name() -> str: """ import hashlib 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() if home == default: 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"]) 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: username, group_name, home_dir = _system_service_identity(run_as_user) diff --git a/hermes_cli/skin_engine.py b/hermes_cli/skin_engine.py index 980ed8b1..62fac0ea 100644 --- a/hermes_cli/skin_engine.py +++ b/hermes_cli/skin_engine.py @@ -101,6 +101,8 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional, Tuple +from hermes_constants import get_hermes_home + logger = logging.getLogger(__name__) @@ -513,8 +515,7 @@ _active_skin_name: str = "default" def _skins_dir() -> Path: """User skins directory.""" - home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) - return home / "skins" + return get_hermes_home() / "skins" def _load_skin_from_yaml(path: Path) -> Optional[Dict[str, Any]]: diff --git a/hermes_cli/uninstall.py b/hermes_cli/uninstall.py index a559a149..333988ed 100644 --- a/hermes_cli/uninstall.py +++ b/hermes_cli/uninstall.py @@ -11,6 +11,8 @@ import shutil import subprocess from pathlib import Path +from hermes_constants import get_hermes_home + from hermes_cli.colors import Colors, color def log_info(msg: str): @@ -31,11 +33,6 @@ def get_project_root() -> Path: 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: """Find shell configuration files that might have PATH entries.""" home = Path.home() diff --git a/hermes_constants.py b/hermes_constants.py index 6a11fb37..51847202 100644 --- a/hermes_constants.py +++ b/hermes_constants.py @@ -4,6 +4,40 @@ Import-safe module with no dependencies — can be imported from anywhere 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": } 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_MODELS_URL = f"{OPENROUTER_BASE_URL}/models" OPENROUTER_CHAT_URL = f"{OPENROUTER_BASE_URL}/chat/completions" diff --git a/hermes_state.py b/hermes_state.py index 5043834c..eb62bc2b 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -21,10 +21,11 @@ import sqlite3 import threading import time from pathlib import Path +from hermes_constants import get_hermes_home 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 diff --git a/hermes_time.py b/hermes_time.py index 97a68881..4ec8dfe0 100644 --- a/hermes_time.py +++ b/hermes_time.py @@ -17,6 +17,7 @@ import logging import os from datetime import datetime from pathlib import Path +from hermes_constants import get_hermes_home from typing import Optional logger = logging.getLogger(__name__) @@ -48,7 +49,7 @@ def _resolve_timezone_name() -> str: # 2. config.yaml ``timezone`` key try: import yaml - hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) + hermes_home = get_hermes_home() config_path = hermes_home / "config.yaml" if config_path.exists(): with open(config_path) as f: diff --git a/honcho_integration/client.py b/honcho_integration/client.py index 12f9a548..385974d1 100644 --- a/honcho_integration/client.py +++ b/honcho_integration/client.py @@ -18,6 +18,8 @@ import os import logging from dataclasses import dataclass, field from pathlib import Path + +from hermes_constants import get_hermes_home from typing import Any, TYPE_CHECKING if TYPE_CHECKING: @@ -29,11 +31,6 @@ GLOBAL_CONFIG_PATH = Path.home() / ".honcho" / "config.json" 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: """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 exists (for first-time setup writes). """ - local_path = _get_hermes_home() / "honcho.json" + local_path = get_hermes_home() / "honcho.json" if local_path.exists(): return local_path return GLOBAL_CONFIG_PATH diff --git a/rl_cli.py b/rl_cli.py index f624333d..03bf015c 100644 --- a/rl_cli.py +++ b/rl_cli.py @@ -29,7 +29,7 @@ import yaml # Load .env from ~/.hermes/.env first, then project root as dev fallback. # 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' from hermes_cli.env_loader import load_hermes_dotenv @@ -60,7 +60,7 @@ from tools.rl_training_tool import get_missing_keys # 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_BASE_URL = OPENROUTER_BASE_URL diff --git a/run_agent.py b/run_agent.py index 3d2c53f1..ef61658d 100644 --- a/run_agent.py +++ b/run_agent.py @@ -45,11 +45,13 @@ import fire from datetime import datetime from pathlib import Path +from hermes_constants import get_hermes_home + # Load .env from ~/.hermes/.env first, then project root as dev fallback. # User-managed env files should override stale shell exports on restart. 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' _loaded_env_paths = load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env) if _loaded_env_paths: @@ -855,7 +857,7 @@ class AIAgent: self.session_id = f"{timestamp_str}_{short_uuid}" # 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.mkdir(parents=True, exist_ok=True) self.session_log_file = self.logs_dir / f"session_{self.session_id}.json" diff --git a/tools/checkpoint_manager.py b/tools/checkpoint_manager.py index 348b436b..35ae5600 100644 --- a/tools/checkpoint_manager.py +++ b/tools/checkpoint_manager.py @@ -24,6 +24,7 @@ import os import shutil import subprocess from pathlib import Path +from hermes_constants import get_hermes_home from typing import Dict, List, Optional, Set logger = logging.getLogger(__name__) @@ -32,7 +33,7 @@ logger = logging.getLogger(__name__) # Constants # --------------------------------------------------------------------------- -CHECKPOINT_BASE = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "checkpoints" +CHECKPOINT_BASE = get_hermes_home() / "checkpoints" DEFAULT_EXCLUDES = [ "node_modules/", diff --git a/tools/memory_tool.py b/tools/memory_tool.py index 241c17f8..a344c874 100644 --- a/tools/memory_tool.py +++ b/tools/memory_tool.py @@ -31,12 +31,13 @@ import re import tempfile from contextlib import contextmanager from pathlib import Path +from hermes_constants import get_hermes_home from typing import Dict, Any, List, Optional logger = logging.getLogger(__name__) # 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" diff --git a/tools/rl_training_tool.py b/tools/rl_training_tool.py index 3ae6c6a4..694b3b80 100644 --- a/tools/rl_training_tool.py +++ b/tools/rl_training_tool.py @@ -44,6 +44,8 @@ from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, List, Optional +from hermes_constants import get_hermes_home + logger = logging.getLogger(__name__) # ============================================================================ @@ -55,7 +57,7 @@ HERMES_ROOT = Path(__file__).parent.parent TINKER_ATROPOS_ROOT = HERMES_ROOT / "tinker-atropos" ENVIRONMENTS_DIR = TINKER_ATROPOS_ROOT / "tinker_atropos" / "environments" 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(): """Lazily create logs directory on first use (avoid side effects at import time).""" diff --git a/tools/skill_manager_tool.py b/tools/skill_manager_tool.py index 7a1a4d63..045e1350 100644 --- a/tools/skill_manager_tool.py +++ b/tools/skill_manager_tool.py @@ -39,6 +39,7 @@ import re import shutil import tempfile from pathlib import Path +from hermes_constants import get_hermes_home from typing import Dict, Any, Optional logger = logging.getLogger(__name__) @@ -76,7 +77,7 @@ import yaml # 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" MAX_NAME_LENGTH = 64 diff --git a/tools/skills_hub.py b/tools/skills_hub.py index f3545981..df9efab6 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -25,6 +25,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path +from hermes_constants import get_hermes_home from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import urlparse, urlunparse @@ -42,7 +43,7 @@ logger = logging.getLogger(__name__) # Paths # --------------------------------------------------------------------------- -HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +HERMES_HOME = get_hermes_home() SKILLS_DIR = HERMES_HOME / "skills" HUB_DIR = SKILLS_DIR / ".hub" LOCK_FILE = HUB_DIR / "lock.json" diff --git a/tools/skills_sync.py b/tools/skills_sync.py index f76fcced..9877afc2 100644 --- a/tools/skills_sync.py +++ b/tools/skills_sync.py @@ -26,12 +26,13 @@ import logging import os import shutil from pathlib import Path +from hermes_constants import get_hermes_home from typing import Dict, List, Tuple logger = logging.getLogger(__name__) -HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +HERMES_HOME = get_hermes_home() SKILLS_DIR = HERMES_HOME / "skills" MANIFEST_FILE = SKILLS_DIR / ".bundled_manifest" diff --git a/tools/skills_tool.py b/tools/skills_tool.py index 5a592ea6..fef89f19 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -68,6 +68,8 @@ Usage: import json import logging + +from hermes_constants import get_hermes_home import os import re import sys @@ -85,7 +87,7 @@ logger = logging.getLogger(__name__) # 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 # 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" # Anthropic-recommended limits for progressive disclosure efficiency diff --git a/tools/tirith_security.py b/tools/tirith_security.py index 2ce5e606..fe021643 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -34,6 +34,8 @@ import threading import time import urllib.request +from hermes_constants import get_hermes_home + logger = logging.getLogger(__name__) _REPO = "sheeki03/tirith" @@ -104,14 +106,8 @@ _MARKER_TTL = 86400 # 24 hours def _get_hermes_home() -> str: - """Return the Hermes home directory, respecting HERMES_HOME env var. - - 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") + """Return the Hermes home directory, respecting HERMES_HOME env var.""" + return str(get_hermes_home()) def _failure_marker_path() -> str: diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index bae0893e..0c0a1fc9 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -32,6 +32,8 @@ import tempfile from pathlib import Path from typing import Optional, Dict, Any +from hermes_constants import get_hermes_home + logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- @@ -83,7 +85,7 @@ def get_stt_model_from_config() -> Optional[str]: """ try: 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(): with open(cfg_path) as f: data = yaml.safe_load(f) or {} diff --git a/tools/tts_tool.py b/tools/tts_tool.py index aab44836..e62353c5 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -33,6 +33,7 @@ import subprocess import tempfile import threading from pathlib import Path +from hermes_constants import get_hermes_home from typing import Callable, Dict, Any, Optional 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_OPENAI_MODEL = "gpt-4o-mini-tts" 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 diff --git a/tools/website_policy.py b/tools/website_policy.py index 2a3d2470..93a2eb28 100644 --- a/tools/website_policy.py +++ b/tools/website_policy.py @@ -19,6 +19,8 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse +from hermes_constants import get_hermes_home + logger = logging.getLogger(__name__) _DEFAULT_WEBSITE_BLOCKLIST = { @@ -36,12 +38,8 @@ _cached_policy_path: Optional[str] = None _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: - return _get_hermes_home() / "config.yaml" + return get_hermes_home() / "config.yaml" class WebsitePolicyError(Exception): @@ -182,7 +180,7 @@ def load_website_blocklist(config_path: Optional[Path] = None) -> Dict[str, Any] continue path = Path(shared_file).expanduser() 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): key = (str(path), normalized) if key in seen: