Merge pull request #1233 from NousResearch/hermes/hermes-7c22e5c1

fix: respect HERMES_HOME in remaining hardcoded paths
This commit is contained in:
Teknium 2026-03-13 21:37:02 -07:00 committed by GitHub
commit 22990ed378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 80 additions and 56 deletions

View file

@ -41,6 +41,7 @@ from typing import Any, Dict, List, Optional, Tuple
from openai import OpenAI from openai import OpenAI
from hermes_cli.config import get_hermes_home
from hermes_constants import OPENROUTER_BASE_URL from hermes_constants import OPENROUTER_BASE_URL
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -73,7 +74,7 @@ auxiliary_is_nous: bool = False
_OPENROUTER_MODEL = "google/gemini-3-flash-preview" _OPENROUTER_MODEL = "google/gemini-3-flash-preview"
_NOUS_MODEL = "gemini-3-flash" _NOUS_MODEL = "gemini-3-flash"
_NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1" _NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1"
_AUTH_JSON_PATH = Path.home() / ".hermes" / "auth.json" _AUTH_JSON_PATH = get_hermes_home() / "auth.json"
# Codex fallback: uses the Responses API (the only endpoint the Codex # Codex fallback: uses the Responses API (the only endpoint the Codex
# OAuth token can access) with a fast model for auxiliary tasks. # OAuth token can access) with a fast model for auxiliary tasks.

View file

@ -420,7 +420,7 @@ def build_context_files_prompt(cwd: Optional[str] = None) -> str:
soul_path = candidate soul_path = candidate
break break
if not soul_path: if not soul_path:
global_soul = Path.home() / ".hermes" / "SOUL.md" global_soul = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "SOUL.md"
if global_soul.exists(): if global_soul.exists():
soul_path = global_soul soul_path = global_soul

26
cli.py
View file

@ -96,7 +96,7 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:
return [] return []
path = Path(file_path).expanduser() path = Path(file_path).expanduser()
if not path.is_absolute(): if not path.is_absolute():
path = Path.home() / ".hermes" / path path = _hermes_home / path
if not path.exists(): if not path.exists():
logger.warning("Prefill messages file not found: %s", path) logger.warning("Prefill messages file not found: %s", path)
return [] return []
@ -141,16 +141,16 @@ def load_cli_config() -> Dict[str, Any]:
Environment variables take precedence over config file values. Environment variables take precedence over config file values.
Returns default values if no config file exists. Returns default values if no config file exists.
""" """
# Check user config first (~/.hermes/config.yaml) # Check user config first ({HERMES_HOME}/config.yaml)
user_config_path = Path.home() / '.hermes' / 'config.yaml' user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml' project_config_path = Path(__file__).parent / 'cli-config.yaml'
# Use user config if it exists, otherwise project config # Use user config if it exists, otherwise project config
if user_config_path.exists(): if user_config_path.exists():
config_path = user_config_path config_path = user_config_path
else: else:
config_path = project_config_path config_path = project_config_path
# Default configuration # Default configuration
defaults = { defaults = {
"model": { "model": {
@ -1037,7 +1037,7 @@ def save_config_value(key_path: str, value: any) -> bool:
True if successful, False otherwise True if successful, False otherwise
""" """
# Use the same precedence as load_cli_config: user config first, then project config # Use the same precedence as load_cli_config: user config first, then project config
user_config_path = Path.home() / '.hermes' / 'config.yaml' user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml' project_config_path = Path(__file__).parent / 'cli-config.yaml'
config_path = user_config_path if user_config_path.exists() else project_config_path config_path = user_config_path if user_config_path.exists() else project_config_path
@ -1259,7 +1259,7 @@ class HermesCLI:
self.session_id = f"{timestamp_str}_{short_uuid}" self.session_id = f"{timestamp_str}_{short_uuid}"
# History file for persistent input recall across sessions # History file for persistent input recall across sessions
self._history_file = Path.home() / ".hermes_history" self._history_file = _hermes_home / ".hermes_history"
self._last_invalidate: float = 0.0 # throttle UI repaints self._last_invalidate: float = 0.0 # throttle UI repaints
self._app = None self._app = None
self._secret_state = None self._secret_state = None
@ -1778,7 +1778,7 @@ class HermesCLI:
""" """
from hermes_cli.clipboard import save_clipboard_image from hermes_cli.clipboard import save_clipboard_image
img_dir = Path.home() / ".hermes" / "images" img_dir = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "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"
@ -2074,7 +2074,7 @@ class HermesCLI:
terminal_cwd = os.getenv("TERMINAL_CWD", os.getcwd()) terminal_cwd = os.getenv("TERMINAL_CWD", os.getcwd())
terminal_timeout = os.getenv("TERMINAL_TIMEOUT", "60") terminal_timeout = os.getenv("TERMINAL_TIMEOUT", "60")
user_config_path = Path.home() / '.hermes' / 'config.yaml' user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml' project_config_path = Path(__file__).parent / 'cli-config.yaml'
if user_config_path.exists(): if user_config_path.exists():
config_path = user_config_path config_path = user_config_path
@ -3684,8 +3684,7 @@ class HermesCLI:
self.agent.interrupt(interrupt_msg) self.agent.interrupt(interrupt_msg)
# Debug: log to file (stdout may be devnull from redirect_stdout) # Debug: log to file (stdout may be devnull from redirect_stdout)
try: try:
import pathlib as _pl _dbg = _hermes_home / "interrupt_debug.log"
_dbg = _pl.Path.home() / ".hermes" / "interrupt_debug.log"
with open(_dbg, "a") as _f: with open(_dbg, "a") as _f:
import time as _t import time as _t
_f.write(f"{_t.strftime('%H:%M:%S')} interrupt fired: msg={str(interrupt_msg)[:60]!r}, " _f.write(f"{_t.strftime('%H:%M:%S')} interrupt fired: msg={str(interrupt_msg)[:60]!r}, "
@ -3993,8 +3992,7 @@ class HermesCLI:
self._interrupt_queue.put(payload) self._interrupt_queue.put(payload)
# Debug: log to file when message enters interrupt queue # Debug: log to file when message enters interrupt queue
try: try:
import pathlib as _pl _dbg = _hermes_home / "interrupt_debug.log"
_dbg = _pl.Path.home() / ".hermes" / "interrupt_debug.log"
with open(_dbg, "a") as _f: with open(_dbg, "a") as _f:
import time as _t import time as _t
_f.write(f"{_t.strftime('%H:%M:%S')} ENTER: queued interrupt msg={str(payload)[:60]!r}, " _f.write(f"{_t.strftime('%H:%M:%S')} ENTER: queued interrupt msg={str(payload)[:60]!r}, "
@ -4255,7 +4253,7 @@ class HermesCLI:
if line_count >= 5 and chars_added > 1 and not text.startswith('/'): if line_count >= 5 and chars_added > 1 and not text.startswith('/'):
_paste_counter[0] += 1 _paste_counter[0] += 1
# Save to temp file # Save to temp file
paste_dir = Path(os.path.expanduser("~/.hermes/pastes")) paste_dir = _hermes_home / "pastes"
paste_dir.mkdir(parents=True, exist_ok=True) paste_dir.mkdir(parents=True, exist_ok=True)
paste_file = paste_dir / f"paste_{_paste_counter[0]}_{datetime.now().strftime('%H%M%S')}.txt" paste_file = paste_dir / f"paste_{_paste_counter[0]}_{datetime.now().strftime('%H%M%S')}.txt"
paste_file.write_text(text, encoding="utf-8") paste_file.write_text(text, encoding="utf-8")

View file

@ -12,9 +12,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DIRECTORY_PATH = Path.home() / ".hermes" / "channel_directory.json" DIRECTORY_PATH = get_hermes_home() / "channel_directory.json"
def _session_entry_id(origin: Dict[str, Any]) -> Optional[str]: def _session_entry_id(origin: Dict[str, Any]) -> Optional[str]:
@ -129,7 +131,7 @@ def _build_slack(adapter) -> List[Dict[str, str]]:
def _build_from_sessions(platform_name: str) -> List[Dict[str, str]]: def _build_from_sessions(platform_name: str) -> List[Dict[str, str]]:
"""Pull known channels/contacts from sessions.json origin data.""" """Pull known channels/contacts from sessions.json origin data."""
sessions_path = Path.home() / ".hermes" / "sessions" / "sessions.json" sessions_path = get_hermes_home() / "sessions" / "sessions.json"
if not sessions_path.exists(): if not sessions_path.exists():
return [] return []

View file

@ -16,6 +16,8 @@ from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from enum import Enum from enum import Enum
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -151,7 +153,7 @@ class GatewayConfig:
reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"]) reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"])
# Storage paths # Storage paths
sessions_dir: Path = field(default_factory=lambda: Path.home() / ".hermes" / "sessions") sessions_dir: Path = field(default_factory=lambda: get_hermes_home() / "sessions")
# Delivery settings # Delivery settings
always_log_local: bool = True # Always save cron outputs to local files always_log_local: bool = True # Always save cron outputs to local files
@ -246,7 +248,7 @@ class GatewayConfig:
if "default_reset_policy" in data: if "default_reset_policy" in data:
default_policy = SessionResetPolicy.from_dict(data["default_reset_policy"]) default_policy = SessionResetPolicy.from_dict(data["default_reset_policy"])
sessions_dir = Path.home() / ".hermes" / "sessions" sessions_dir = get_hermes_home() / "sessions"
if "sessions_dir" in data: if "sessions_dir" in data:
sessions_dir = Path(data["sessions_dir"]) sessions_dir = Path(data["sessions_dir"])
@ -274,7 +276,8 @@ def load_gateway_config() -> GatewayConfig:
config = GatewayConfig() config = GatewayConfig()
# Try loading from ~/.hermes/gateway.json # Try loading from ~/.hermes/gateway.json
gateway_config_path = Path.home() / ".hermes" / "gateway.json" _home = get_hermes_home()
gateway_config_path = _home / "gateway.json"
if gateway_config_path.exists(): if gateway_config_path.exists():
try: try:
with open(gateway_config_path, "r", encoding="utf-8") as f: with open(gateway_config_path, "r", encoding="utf-8") as f:
@ -282,13 +285,13 @@ def load_gateway_config() -> GatewayConfig:
config = GatewayConfig.from_dict(data) config = GatewayConfig.from_dict(data)
except Exception as e: except Exception as e:
print(f"[gateway] Warning: Failed to load {gateway_config_path}: {e}") print(f"[gateway] Warning: Failed to load {gateway_config_path}: {e}")
# Bridge session_reset from config.yaml (the user-facing config file) # Bridge session_reset from config.yaml (the user-facing config file)
# into the gateway config. config.yaml takes precedence over gateway.json # into the gateway config. config.yaml takes precedence over gateway.json
# for session reset policy since that's where hermes setup writes it. # for session reset policy since that's where hermes setup writes it.
try: try:
import yaml import yaml
config_yaml_path = Path.home() / ".hermes" / "config.yaml" config_yaml_path = _home / "config.yaml"
if config_yaml_path.exists(): if config_yaml_path.exists():
with open(config_yaml_path, encoding="utf-8") as f: with open(config_yaml_path, encoding="utf-8") as f:
yaml_cfg = yaml.safe_load(f) or {} yaml_cfg = yaml.safe_load(f) or {}
@ -481,7 +484,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
def save_gateway_config(config: GatewayConfig) -> None: def save_gateway_config(config: GatewayConfig) -> None:
"""Save gateway configuration to ~/.hermes/gateway.json.""" """Save gateway configuration to ~/.hermes/gateway.json."""
gateway_config_path = Path.home() / ".hermes" / "gateway.json" gateway_config_path = get_hermes_home() / "gateway.json"
gateway_config_path.parent.mkdir(parents=True, exist_ok=True) gateway_config_path.parent.mkdir(parents=True, exist_ok=True)
with open(gateway_config_path, "w", encoding="utf-8") as f: with open(gateway_config_path, "w", encoding="utf-8") as f:

View file

@ -15,6 +15,8 @@ from dataclasses import dataclass
from typing import Dict, List, Optional, Any, Union from typing import Dict, List, Optional, Any, Union
from enum import Enum from enum import Enum
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_PLATFORM_OUTPUT = 4000 MAX_PLATFORM_OUTPUT = 4000
@ -116,7 +118,7 @@ class DeliveryRouter:
""" """
self.config = config self.config = config
self.adapters = adapters or {} self.adapters = adapters or {}
self.output_dir = Path.home() / ".hermes" / "cron" / "output" self.output_dir = get_hermes_home() / "cron" / "output"
def resolve_targets( def resolve_targets(
self, self,
@ -256,7 +258,7 @@ class DeliveryRouter:
def _save_full_output(self, content: str, job_id: str) -> Path: def _save_full_output(self, content: str, job_id: str) -> Path:
"""Save full cron output to disk and return the file path.""" """Save full cron output to disk and return the file path."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
out_dir = Path.home() / ".hermes" / "cron" / "output" out_dir = get_hermes_home() / "cron" / "output"
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / f"{job_id}_{timestamp}.txt" path = out_dir / f"{job_id}_{timestamp}.txt"
path.write_text(content) path.write_text(content)

View file

@ -26,8 +26,10 @@ from typing import Any, Callable, Dict, List, Optional
import yaml import yaml
from hermes_cli.config import get_hermes_home
HOOKS_DIR = Path(os.path.expanduser("~/.hermes/hooks"))
HOOKS_DIR = get_hermes_home() / "hooks"
class HookRegistry: class HookRegistry:

View file

@ -15,9 +15,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SESSIONS_DIR = Path.home() / ".hermes" / "sessions" _SESSIONS_DIR = get_hermes_home() / "sessions"
_SESSIONS_INDEX = _SESSIONS_DIR / "sessions.json" _SESSIONS_INDEX = _SESSIONS_DIR / "sessions.json"

View file

@ -25,6 +25,8 @@ import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from hermes_cli.config import get_hermes_home
# Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion # Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion
ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
@ -39,7 +41,7 @@ LOCKOUT_SECONDS = 3600 # Lockout duration after too many failures
MAX_PENDING_PER_PLATFORM = 3 # Max pending codes per platform MAX_PENDING_PER_PLATFORM = 3 # Max pending codes per platform
MAX_FAILED_ATTEMPTS = 5 # Failed approvals before lockout MAX_FAILED_ATTEMPTS = 5 # Failed approvals before lockout
PAIRING_DIR = Path(os.path.expanduser("~/.hermes/pairing")) PAIRING_DIR = get_hermes_home() / "pairing"
def _secure_write(path: Path, data: str) -> None: def _secure_write(path: Path, data: str) -> None:

View file

@ -25,6 +25,7 @@ sys.path.insert(0, str(_Path(__file__).resolve().parents[2]))
from gateway.config import Platform, PlatformConfig from gateway.config import Platform, PlatformConfig
from gateway.session import SessionSource, build_session_key from gateway.session import SessionSource, build_session_key
from hermes_cli.config import get_hermes_home
GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = ( GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
@ -42,8 +43,8 @@ GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
# (e.g. Telegram file URLs expire after ~1 hour). # (e.g. Telegram file URLs expire after ~1 hour).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Default location: ~/.hermes/image_cache/ # Default location: {HERMES_HOME}/image_cache/
IMAGE_CACHE_DIR = Path(os.path.expanduser("~/.hermes/image_cache")) IMAGE_CACHE_DIR = get_hermes_home() / "image_cache"
def get_image_cache_dir() -> Path: def get_image_cache_dir() -> Path:
@ -125,7 +126,7 @@ def cleanup_image_cache(max_age_hours: int = 24) -> int:
# here so the STT tool (OpenAI Whisper) can transcribe them from local files. # here so the STT tool (OpenAI Whisper) can transcribe them from local files.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
AUDIO_CACHE_DIR = Path(os.path.expanduser("~/.hermes/audio_cache")) AUDIO_CACHE_DIR = get_hermes_home() / "audio_cache"
def get_audio_cache_dir() -> Path: def get_audio_cache_dir() -> Path:
@ -184,7 +185,7 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg") -> str:
# here so the agent can reference them by local file path. # here so the agent can reference them by local file path.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
DOCUMENT_CACHE_DIR = Path(os.path.expanduser("~/.hermes/document_cache")) DOCUMENT_CACHE_DIR = get_hermes_home() / "document_cache"
SUPPORTED_DOCUMENT_TYPES = { SUPPORTED_DOCUMENT_TYPES = {
".pdf": "application/pdf", ".pdf": "application/pdf",

View file

@ -26,6 +26,8 @@ _IS_WINDOWS = platform.system() == "Windows"
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -132,7 +134,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
) )
self._session_path: Path = Path(config.extra.get( self._session_path: Path = Path(config.extra.get(
"session_path", "session_path",
Path.home() / ".hermes" / "whatsapp" / "session" get_hermes_home() / "whatsapp" / "session"
)) ))
self._message_queue: asyncio.Queue = asyncio.Queue() self._message_queue: asyncio.Queue = asyncio.Queue()
self._bridge_log_fh = None self._bridge_log_fh = None

View file

@ -14,8 +14,10 @@ import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from hermes_cli.config import get_hermes_home
CACHE_PATH = Path(os.path.expanduser("~/.hermes/sticker_cache.json"))
CACHE_PATH = get_hermes_home() / "sticker_cache.json"
# Vision prompt for describing stickers -- kept concise to save tokens # Vision prompt for describing stickers -- kept concise to save tokens
STICKER_VISION_PROMPT = ( STICKER_VISION_PROMPT = (

View file

@ -13,7 +13,7 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent.resolve() PROJECT_ROOT = Path(__file__).parent.parent.resolve()
from hermes_cli.config import get_env_value, save_env_value from hermes_cli.config import get_env_value, get_hermes_home, save_env_value
from hermes_cli.setup import ( from hermes_cli.setup import (
print_header, print_info, print_success, print_warning, print_error, print_header, print_info, print_success, print_warning, print_error,
prompt, prompt_choice, prompt_yes_no, prompt, prompt_choice, prompt_yes_no,
@ -283,7 +283,7 @@ def systemd_status(deep: bool = False):
def generate_launchd_plist() -> str: def generate_launchd_plist() -> str:
python_path = get_python_path() python_path = get_python_path()
working_dir = str(PROJECT_ROOT) working_dir = str(PROJECT_ROOT)
log_dir = Path.home() / ".hermes" / "logs" log_dir = get_hermes_home() / "logs"
log_dir.mkdir(parents=True, exist_ok=True) log_dir.mkdir(parents=True, exist_ok=True)
return f"""<?xml version="1.0" encoding="UTF-8"?> return f"""<?xml version="1.0" encoding="UTF-8"?>
@ -380,7 +380,7 @@ def launchd_status(deep: bool = False):
print("✗ Gateway service is not loaded") print("✗ Gateway service is not loaded")
if deep: if deep:
log_file = Path.home() / ".hermes" / "logs" / "gateway.log" log_file = get_hermes_home() / "logs" / "gateway.log"
if log_file.exists(): if log_file.exists():
print() print()
print("Recent logs:") print("Recent logs:")
@ -557,7 +557,7 @@ def _platform_status(platform: dict) -> str:
val = get_env_value(token_var) val = get_env_value(token_var)
if token_var == "WHATSAPP_ENABLED": if token_var == "WHATSAPP_ENABLED":
if val and val.lower() == "true": if val and val.lower() == "true":
session_file = Path.home() / ".hermes" / "whatsapp" / "session" / "creds.json" session_file = get_hermes_home() / "whatsapp" / "session" / "creds.json"
if session_file.exists(): if session_file.exists():
return "configured + paired" return "configured + paired"
return "enabled, not paired" return "enabled, not paired"

View file

@ -648,7 +648,7 @@ def cmd_whatsapp(args):
print("✓ Bridge dependencies already installed") print("✓ Bridge dependencies already installed")
# ── Step 5: Check for existing session ─────────────────────────────── # ── Step 5: Check for existing session ───────────────────────────────
session_dir = Path.home() / ".hermes" / "whatsapp" / "session" session_dir = get_hermes_home() / "whatsapp" / "session"
session_dir.mkdir(parents=True, exist_ok=True) session_dir.mkdir(parents=True, exist_ok=True)
if (session_dir / "creds.json").exists(): if (session_dir / "creds.json").exists():

View file

@ -12,7 +12,7 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent.resolve() PROJECT_ROOT = Path(__file__).parent.parent.resolve()
from hermes_cli.colors import Colors, color from hermes_cli.colors import Colors, color
from hermes_cli.config import get_env_path, get_env_value from hermes_cli.config import get_env_path, get_env_value, get_hermes_home
from hermes_constants import OPENROUTER_MODELS_URL from hermes_constants import OPENROUTER_MODELS_URL
def check_mark(ok: bool) -> str: def check_mark(ok: bool) -> str:
@ -267,7 +267,7 @@ def show_status(args):
print() print()
print(color("◆ Scheduled Jobs", Colors.CYAN, Colors.BOLD)) print(color("◆ Scheduled Jobs", Colors.CYAN, Colors.BOLD))
jobs_file = Path.home() / ".hermes" / "cron" / "jobs.json" jobs_file = get_hermes_home() / "cron" / "jobs.json"
if jobs_file.exists(): if jobs_file.exists():
import json import json
try: try:
@ -287,7 +287,7 @@ def show_status(args):
print() print()
print(color("◆ Sessions", Colors.CYAN, Colors.BOLD)) print(color("◆ Sessions", Colors.CYAN, Colors.BOLD))
sessions_file = Path.home() / ".hermes" / "sessions" / "sessions.json" sessions_file = get_hermes_home() / "sessions" / "sessions.json"
if sessions_file.exists(): if sessions_file.exists():
import json import json
try: try:

View file

@ -408,7 +408,7 @@ class AIAgent:
# Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log # Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log
# so tool failures, API errors, etc. are inspectable after the fact. # so tool failures, API errors, etc. are inspectable after the fact.
from agent.redact import RedactingFormatter from agent.redact import RedactingFormatter
_error_log_dir = Path.home() / ".hermes" / "logs" _error_log_dir = _hermes_home / "logs"
_error_log_dir.mkdir(parents=True, exist_ok=True) _error_log_dir.mkdir(parents=True, exist_ok=True)
_error_log_path = _error_log_dir / "errors.log" _error_log_path = _error_log_dir / "errors.log"
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler

View file

@ -1,6 +1,7 @@
"""Tests for gateway/channel_directory.py — channel resolution and display.""" """Tests for gateway/channel_directory.py — channel resolution and display."""
import json import json
import os
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
@ -122,7 +123,7 @@ class TestResolveChannelName:
class TestBuildFromSessions: class TestBuildFromSessions:
def _write_sessions(self, tmp_path, sessions_data): def _write_sessions(self, tmp_path, sessions_data):
"""Write sessions.json at the path _build_from_sessions expects.""" """Write sessions.json at the path _build_from_sessions expects."""
sessions_path = tmp_path / ".hermes" / "sessions" / "sessions.json" sessions_path = tmp_path / "sessions" / "sessions.json"
sessions_path.parent.mkdir(parents=True) sessions_path.parent.mkdir(parents=True)
sessions_path.write_text(json.dumps(sessions_data)) sessions_path.write_text(json.dumps(sessions_data))
@ -152,7 +153,7 @@ class TestBuildFromSessions:
}, },
}) })
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
assert len(entries) == 2 assert len(entries) == 2
@ -161,7 +162,7 @@ class TestBuildFromSessions:
assert "Bob" in names assert "Bob" in names
def test_missing_sessions_file(self, tmp_path): def test_missing_sessions_file(self, tmp_path):
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
assert entries == [] assert entries == []
@ -171,7 +172,7 @@ class TestBuildFromSessions:
"s2": {"origin": {"platform": "telegram", "chat_id": "123", "chat_name": "X"}}, "s2": {"origin": {"platform": "telegram", "chat_id": "123", "chat_name": "X"}},
}) })
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
assert len(entries) == 1 assert len(entries) == 1
@ -202,7 +203,7 @@ class TestBuildFromSessions:
}, },
}) })
with patch.object(Path, "home", return_value=tmp_path): with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
entries = _build_from_sessions("telegram") entries = _build_from_sessions("telegram")
ids = {entry["id"] for entry in entries} ids = {entry["id"] for entry in entries}

View file

@ -808,7 +808,7 @@ class TestTryAttachClipboardImage:
with patch("hermes_cli.clipboard.save_clipboard_image", return_value=True): with patch("hermes_cli.clipboard.save_clipboard_image", return_value=True):
cli._try_attach_clipboard_image() cli._try_attach_clipboard_image()
path = cli._attached_images[0] path = cli._attached_images[0]
assert path.parent == Path.home() / ".hermes" / "images" assert path.parent == Path(os.environ["HERMES_HOME"]) / "images"
assert path.name.startswith("clip_") assert path.name.startswith("clip_")
assert path.suffix == ".png" assert path.suffix == ".png"

View file

@ -5,18 +5,20 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from hermes_cli.config import get_hermes_home
def get_sandbox_dir() -> Path: def get_sandbox_dir() -> Path:
"""Return the host-side root for all sandbox storage (Docker workspaces, """Return the host-side root for all sandbox storage (Docker workspaces,
Singularity overlays/SIF cache, etc.). Singularity overlays/SIF cache, etc.).
Configurable via TERMINAL_SANDBOX_DIR. Defaults to ~/.hermes/sandboxes/. Configurable via TERMINAL_SANDBOX_DIR. Defaults to {HERMES_HOME}/sandboxes/.
""" """
custom = os.getenv("TERMINAL_SANDBOX_DIR") custom = os.getenv("TERMINAL_SANDBOX_DIR")
if custom: if custom:
p = Path(custom) p = Path(custom)
else: else:
p = Path.home() / ".hermes" / "sandboxes" p = get_hermes_home() / "sandboxes"
p.mkdir(parents=True, exist_ok=True) p.mkdir(parents=True, exist_ok=True)
return p return p

View file

@ -13,12 +13,13 @@ import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from hermes_cli.config import get_hermes_home
from tools.environments.base import BaseEnvironment from tools.environments.base import BaseEnvironment
from tools.interrupt import is_interrupted from tools.interrupt import is_interrupted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SNAPSHOT_STORE = Path.home() / ".hermes" / "modal_snapshots.json" _SNAPSHOT_STORE = get_hermes_home() / "modal_snapshots.json"
def _load_snapshots() -> Dict[str, str]: def _load_snapshots() -> Dict[str, str]:

View file

@ -16,12 +16,13 @@ import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from hermes_cli.config import get_hermes_home
from tools.environments.base import BaseEnvironment from tools.environments.base import BaseEnvironment
from tools.interrupt import is_interrupted from tools.interrupt import is_interrupted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SNAPSHOT_STORE = Path.home() / ".hermes" / "singularity_snapshots.json" _SNAPSHOT_STORE = get_hermes_home() / "singularity_snapshots.json"
def _load_snapshots() -> Dict[str, str]: def _load_snapshots() -> Dict[str, str]:

View file

@ -47,11 +47,13 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from hermes_cli.config import get_hermes_home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Checkpoint file for crash recovery (gateway only) # Checkpoint file for crash recovery (gateway only)
CHECKPOINT_PATH = Path(os.path.expanduser("~/.hermes/processes.json")) CHECKPOINT_PATH = get_hermes_home() / "processes.json"
# Limits # Limits
MAX_OUTPUT_CHARS = 200_000 # 200KB rolling output buffer MAX_OUTPUT_CHARS = 200_000 # 200KB rolling output buffer

View file

@ -65,7 +65,7 @@ DEFAULT_ELEVENLABS_VOICE_ID = "pNInz6obpgDQGcFmaJgB" # Adam
DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2" DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"
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 = os.path.expanduser("~/.hermes/audio_cache") DEFAULT_OUTPUT_DIR = str(Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "audio_cache")
MAX_TEXT_LENGTH = 4000 MAX_TEXT_LENGTH = 4000