Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

from openai import OpenAI

from hermes_cli.config import get_hermes_home
from hermes_constants import OPENROUTER_BASE_URL

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,7 +74,7 @@
_OPENROUTER_MODEL = "google/gemini-3-flash-preview"
_NOUS_MODEL = "gemini-3-flash"
_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
# OAuth token can access) with a fast model for auxiliary tasks.
Expand Down
2 changes: 1 addition & 1 deletion agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def build_context_files_prompt(cwd: Optional[str] = None) -> str:
soul_path = candidate
break
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():
soul_path = global_soul

Expand Down
26 changes: 12 additions & 14 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:
return []
path = Path(file_path).expanduser()
if not path.is_absolute():
path = Path.home() / ".hermes" / path
path = _hermes_home / path
if not path.exists():
logger.warning("Prefill messages file not found: %s", path)
return []
Expand Down Expand Up @@ -141,16 +141,16 @@ def load_cli_config() -> Dict[str, Any]:
Environment variables take precedence over config file values.
Returns default values if no config file exists.
"""
# Check user config first (~/.hermes/config.yaml)
user_config_path = Path.home() / '.hermes' / 'config.yaml'
# Check user config first ({HERMES_HOME}/config.yaml)
user_config_path = _hermes_home / 'config.yaml'
project_config_path = Path(__file__).parent / 'cli-config.yaml'

# Use user config if it exists, otherwise project config
if user_config_path.exists():
config_path = user_config_path
else:
config_path = project_config_path

# Default configuration
defaults = {
"model": {
Expand Down Expand Up @@ -1037,7 +1037,7 @@ def save_config_value(key_path: str, value: any) -> bool:
True if successful, False otherwise
"""
# 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'
config_path = user_config_path if user_config_path.exists() else project_config_path

Expand Down Expand Up @@ -1259,7 +1259,7 @@ def __init__(
self.session_id = f"{timestamp_str}_{short_uuid}"

# 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._app = None
self._secret_state = None
Expand Down Expand Up @@ -1778,7 +1778,7 @@ def _try_attach_clipboard_image(self) -> bool:
"""
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
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
img_path = img_dir / f"clip_{ts}_{self._image_counter}.png"
Expand Down Expand Up @@ -2074,7 +2074,7 @@ def show_config(self):
terminal_cwd = os.getenv("TERMINAL_CWD", os.getcwd())
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'
if user_config_path.exists():
config_path = user_config_path
Expand Down Expand Up @@ -3684,8 +3684,7 @@ def run_agent():
self.agent.interrupt(interrupt_msg)
# Debug: log to file (stdout may be devnull from redirect_stdout)
try:
import pathlib as _pl
_dbg = _pl.Path.home() / ".hermes" / "interrupt_debug.log"
_dbg = _hermes_home / "interrupt_debug.log"
with open(_dbg, "a") as _f:
import time as _t
_f.write(f"{_t.strftime('%H:%M:%S')} interrupt fired: msg={str(interrupt_msg)[:60]!r}, "
Expand Down Expand Up @@ -3993,8 +3992,7 @@ def handle_enter(event):
self._interrupt_queue.put(payload)
# Debug: log to file when message enters interrupt queue
try:
import pathlib as _pl
_dbg = _pl.Path.home() / ".hermes" / "interrupt_debug.log"
_dbg = _hermes_home / "interrupt_debug.log"
with open(_dbg, "a") as _f:
import time as _t
_f.write(f"{_t.strftime('%H:%M:%S')} ENTER: queued interrupt msg={str(payload)[:60]!r}, "
Expand Down Expand Up @@ -4255,7 +4253,7 @@ def _on_text_changed(buf):
if line_count >= 5 and chars_added > 1 and not text.startswith('/'):
_paste_counter[0] += 1
# 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_file = paste_dir / f"paste_{_paste_counter[0]}_{datetime.now().strftime('%H%M%S')}.txt"
paste_file.write_text(text, encoding="utf-8")
Expand Down
6 changes: 4 additions & 2 deletions gateway/channel_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

from hermes_cli.config import get_hermes_home

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]:
Expand Down Expand Up @@ -129,7 +131,7 @@ def _build_slack(adapter) -> List[Dict[str, str]]:

def _build_from_sessions(platform_name: str) -> List[Dict[str, str]]:
"""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():
return []

Expand Down
15 changes: 9 additions & 6 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from typing import Dict, List, Optional, Any
from enum import Enum

from hermes_cli.config import get_hermes_home

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -151,7 +153,7 @@ class GatewayConfig:
reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"])

# 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
always_log_local: bool = True # Always save cron outputs to local files
Expand Down Expand Up @@ -246,7 +248,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "GatewayConfig":
if "default_reset_policy" in data:
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:
sessions_dir = Path(data["sessions_dir"])

Expand Down Expand Up @@ -274,21 +276,22 @@ def load_gateway_config() -> GatewayConfig:
config = GatewayConfig()

# 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():
try:
with open(gateway_config_path, "r", encoding="utf-8") as f:
data = json.load(f)
config = GatewayConfig.from_dict(data)
except Exception as e:
print(f"[gateway] Warning: Failed to load {gateway_config_path}: {e}")

# Bridge session_reset from config.yaml (the user-facing config file)
# into the gateway config. config.yaml takes precedence over gateway.json
# for session reset policy since that's where hermes setup writes it.
try:
import yaml
config_yaml_path = Path.home() / ".hermes" / "config.yaml"
config_yaml_path = _home / "config.yaml"
if config_yaml_path.exists():
with open(config_yaml_path, encoding="utf-8") as f:
yaml_cfg = yaml.safe_load(f) or {}
Expand Down Expand Up @@ -481,7 +484,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:

def save_gateway_config(config: GatewayConfig) -> None:
"""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)

with open(gateway_config_path, "w", encoding="utf-8") as f:
Expand Down
6 changes: 4 additions & 2 deletions gateway/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from typing import Dict, List, Optional, Any, Union
from enum import Enum

from hermes_cli.config import get_hermes_home

logger = logging.getLogger(__name__)

MAX_PLATFORM_OUTPUT = 4000
Expand Down Expand Up @@ -116,7 +118,7 @@ def __init__(self, config: GatewayConfig, adapters: Dict[Platform, Any] = None):
"""
self.config = config
self.adapters = adapters or {}
self.output_dir = Path.home() / ".hermes" / "cron" / "output"
self.output_dir = get_hermes_home() / "cron" / "output"

def resolve_targets(
self,
Expand Down Expand Up @@ -256,7 +258,7 @@ def _deliver_local(
def _save_full_output(self, content: str, job_id: str) -> Path:
"""Save full cron output to disk and return the file path."""
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)
path = out_dir / f"{job_id}_{timestamp}.txt"
path.write_text(content)
Expand Down
4 changes: 3 additions & 1 deletion gateway/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@

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:
Expand Down
4 changes: 3 additions & 1 deletion gateway/mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
from pathlib import Path
from typing import Optional

from hermes_cli.config import get_hermes_home

logger = logging.getLogger(__name__)

_SESSIONS_DIR = Path.home() / ".hermes" / "sessions"
_SESSIONS_DIR = get_hermes_home() / "sessions"
_SESSIONS_INDEX = _SESSIONS_DIR / "sessions.json"


Expand Down
4 changes: 3 additions & 1 deletion gateway/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from pathlib import Path
from typing import Optional

from hermes_cli.config import get_hermes_home


# Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion
ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
Expand All @@ -39,7 +41,7 @@
MAX_PENDING_PER_PLATFORM = 3 # Max pending codes per platform
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:
Expand Down
9 changes: 5 additions & 4 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from gateway.config import Platform, PlatformConfig
from gateway.session import SessionSource, build_session_key
from hermes_cli.config import get_hermes_home


GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
Expand All @@ -42,8 +43,8 @@
# (e.g. Telegram file URLs expire after ~1 hour).
# ---------------------------------------------------------------------------

# Default location: ~/.hermes/image_cache/
IMAGE_CACHE_DIR = Path(os.path.expanduser("~/.hermes/image_cache"))
# Default location: {HERMES_HOME}/image_cache/
IMAGE_CACHE_DIR = get_hermes_home() / "image_cache"


def get_image_cache_dir() -> Path:
Expand Down Expand Up @@ -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.
# ---------------------------------------------------------------------------

AUDIO_CACHE_DIR = Path(os.path.expanduser("~/.hermes/audio_cache"))
AUDIO_CACHE_DIR = get_hermes_home() / "audio_cache"


def get_audio_cache_dir() -> Path:
Expand Down Expand Up @@ -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.
# ---------------------------------------------------------------------------

DOCUMENT_CACHE_DIR = Path(os.path.expanduser("~/.hermes/document_cache"))
DOCUMENT_CACHE_DIR = get_hermes_home() / "document_cache"

SUPPORTED_DOCUMENT_TYPES = {
".pdf": "application/pdf",
Expand Down
4 changes: 3 additions & 1 deletion gateway/platforms/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from pathlib import Path
from typing import Dict, List, Optional, Any

from hermes_cli.config import get_hermes_home

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -132,7 +134,7 @@ def __init__(self, config: PlatformConfig):
)
self._session_path: Path = Path(config.extra.get(
"session_path",
Path.home() / ".hermes" / "whatsapp" / "session"
get_hermes_home() / "whatsapp" / "session"
))
self._message_queue: asyncio.Queue = asyncio.Queue()
self._bridge_log_fh = None
Expand Down
4 changes: 3 additions & 1 deletion gateway/sticker_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
from pathlib import Path
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
STICKER_VISION_PROMPT = (
Expand Down
8 changes: 4 additions & 4 deletions hermes_cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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 (
print_header, print_info, print_success, print_warning, print_error,
prompt, prompt_choice, prompt_yes_no,
Expand Down Expand Up @@ -283,7 +283,7 @@ def systemd_status(deep: bool = False):
def generate_launchd_plist() -> str:
python_path = get_python_path()
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)

return f"""<?xml version="1.0" encoding="UTF-8"?>
Expand Down Expand Up @@ -380,7 +380,7 @@ def launchd_status(deep: bool = False):
print("✗ Gateway service is not loaded")

if deep:
log_file = Path.home() / ".hermes" / "logs" / "gateway.log"
log_file = get_hermes_home() / "logs" / "gateway.log"
if log_file.exists():
print()
print("Recent logs:")
Expand Down Expand Up @@ -557,7 +557,7 @@ def _platform_status(platform: dict) -> str:
val = get_env_value(token_var)
if token_var == "WHATSAPP_ENABLED":
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():
return "configured + paired"
return "enabled, not paired"
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ def cmd_whatsapp(args):
print("✓ Bridge dependencies already installed")

# ── 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)

if (session_dir / "creds.json").exists():
Expand Down
Loading
Loading