refactor: consolidate ~/.hermes directory layout with backward compat (#3610)
New installs get a cleaner structure: cache/images/ (was image_cache/) cache/audio/ (was audio_cache/) cache/documents/ (was document_cache/) cache/screenshots/ (was browser_screenshots/) platforms/whatsapp/session/ (was whatsapp/session/) platforms/matrix/store/ (was matrix/store/) platforms/pairing/ (was pairing/) Existing installs are unaffected -- get_hermes_dir() checks for the old path first and uses it if present. No migration needed. Adds get_hermes_dir(new_subpath, old_name) helper to hermes_constants.py for reuse by any future subsystem.
This commit is contained in:
@@ -25,7 +25,7 @@ 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
|
from hermes_constants import get_hermes_dir
|
||||||
|
|
||||||
|
|
||||||
# Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion
|
# Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion
|
||||||
@@ -41,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 = get_hermes_home() / "pairing"
|
PAIRING_DIR = get_hermes_dir("platforms/pairing", "pairing")
|
||||||
|
|
||||||
|
|
||||||
def _secure_write(path: Path, data: str) -> None:
|
def _secure_write(path: Path, data: str) -> None:
|
||||||
|
|||||||
@@ -27,6 +27,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
|
from hermes_cli.config import get_hermes_home
|
||||||
|
from hermes_constants import get_hermes_dir
|
||||||
|
|
||||||
|
|
||||||
GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
|
GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
|
||||||
@@ -44,8 +45,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_HOME}/image_cache/
|
# Default location: {HERMES_HOME}/cache/images/ (legacy: image_cache/)
|
||||||
IMAGE_CACHE_DIR = get_hermes_home() / "image_cache"
|
IMAGE_CACHE_DIR = get_hermes_dir("cache/images", "image_cache")
|
||||||
|
|
||||||
|
|
||||||
def get_image_cache_dir() -> Path:
|
def get_image_cache_dir() -> Path:
|
||||||
@@ -147,7 +148,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 = get_hermes_home() / "audio_cache"
|
AUDIO_CACHE_DIR = get_hermes_dir("cache/audio", "audio_cache")
|
||||||
|
|
||||||
|
|
||||||
def get_audio_cache_dir() -> Path:
|
def get_audio_cache_dir() -> Path:
|
||||||
@@ -206,7 +207,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 = get_hermes_home() / "document_cache"
|
DOCUMENT_CACHE_DIR = get_hermes_dir("cache/documents", "document_cache")
|
||||||
|
|
||||||
SUPPORTED_DOCUMENT_TYPES = {
|
SUPPORTED_DOCUMENT_TYPES = {
|
||||||
".pdf": "application/pdf",
|
".pdf": "application/pdf",
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ MAX_MESSAGE_LENGTH = 4000
|
|||||||
|
|
||||||
# Store directory for E2EE keys and sync state.
|
# Store directory for E2EE keys and sync state.
|
||||||
# Uses get_hermes_home() so each profile gets its own Matrix store.
|
# Uses get_hermes_home() so each profile gets its own Matrix store.
|
||||||
from hermes_constants import get_hermes_home as _get_hermes_home
|
from hermes_constants import get_hermes_dir as _get_hermes_dir
|
||||||
_STORE_DIR = _get_hermes_home() / "matrix" / "store"
|
_STORE_DIR = _get_hermes_dir("platforms/matrix/store", "matrix/store")
|
||||||
|
|
||||||
# Grace period: ignore messages older than this many seconds before startup.
|
# Grace period: ignore messages older than this many seconds before startup.
|
||||||
_STARTUP_GRACE_SECONDS = 5
|
_STARTUP_GRACE_SECONDS = 5
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from pathlib import Path
|
|||||||
from typing import Dict, Optional, Any
|
from typing import Dict, Optional, Any
|
||||||
|
|
||||||
from hermes_cli.config import get_hermes_home
|
from hermes_cli.config import get_hermes_home
|
||||||
|
from hermes_constants import get_hermes_dir
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
|||||||
)
|
)
|
||||||
self._session_path: Path = Path(config.extra.get(
|
self._session_path: Path = Path(config.extra.get(
|
||||||
"session_path",
|
"session_path",
|
||||||
get_hermes_home() / "whatsapp" / "session"
|
get_hermes_dir("platforms/whatsapp/session", "whatsapp/session")
|
||||||
))
|
))
|
||||||
self._reply_prefix: Optional[str] = config.extra.get("reply_prefix")
|
self._reply_prefix: Optional[str] = config.extra.get("reply_prefix")
|
||||||
self._message_queue: asyncio.Queue = asyncio.Queue()
|
self._message_queue: asyncio.Queue = asyncio.Queue()
|
||||||
|
|||||||
@@ -17,6 +17,27 @@ def get_hermes_home() -> Path:
|
|||||||
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_hermes_dir(new_subpath: str, old_name: str) -> Path:
|
||||||
|
"""Resolve a Hermes subdirectory with backward compatibility.
|
||||||
|
|
||||||
|
New installs get the consolidated layout (e.g. ``cache/images``).
|
||||||
|
Existing installs that already have the old path (e.g. ``image_cache``)
|
||||||
|
keep using it — no migration required.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_subpath: Preferred path relative to HERMES_HOME (e.g. ``"cache/images"``).
|
||||||
|
old_name: Legacy path relative to HERMES_HOME (e.g. ``"image_cache"``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute ``Path`` — old location if it exists on disk, otherwise the new one.
|
||||||
|
"""
|
||||||
|
home = get_hermes_home()
|
||||||
|
old_path = home / old_name
|
||||||
|
if old_path.exists():
|
||||||
|
return old_path
|
||||||
|
return home / new_subpath
|
||||||
|
|
||||||
|
|
||||||
VALID_REASONING_EFFORTS = ("xhigh", "high", "medium", "low", "minimal")
|
VALID_REASONING_EFFORTS = ("xhigh", "high", "medium", "low", "minimal")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1523,8 +1523,8 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
|
|||||||
effective_task_id = task_id or "default"
|
effective_task_id = task_id or "default"
|
||||||
|
|
||||||
# Save screenshot to persistent location so it can be shared with users
|
# Save screenshot to persistent location so it can be shared with users
|
||||||
hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
from hermes_constants import get_hermes_dir
|
||||||
screenshots_dir = hermes_home / "browser_screenshots"
|
screenshots_dir = get_hermes_dir("cache/screenshots", "browser_screenshots")
|
||||||
screenshot_path = screenshots_dir / f"browser_screenshot_{uuid_mod.uuid4().hex}.png"
|
screenshot_path = screenshots_dir / f"browser_screenshot_{uuid_mod.uuid4().hex}.png"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ 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(get_hermes_home() / "audio_cache")
|
def _get_default_output_dir() -> str:
|
||||||
|
from hermes_constants import get_hermes_dir
|
||||||
|
return str(get_hermes_dir("cache/audio", "audio_cache"))
|
||||||
|
|
||||||
|
DEFAULT_OUTPUT_DIR = _get_default_output_dir()
|
||||||
MAX_TEXT_LENGTH = 4000
|
MAX_TEXT_LENGTH = 4000
|
||||||
|
|
||||||
|
|
||||||
@@ -828,7 +832,7 @@ TTS_SCHEMA = {
|
|||||||
},
|
},
|
||||||
"output_path": {
|
"output_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional custom file path to save the audio. Defaults to ~/.hermes/audio_cache/<timestamp>.mp3"
|
"description": "Optional custom file path to save the audio. Defaults to ~/.hermes/cache/audio/<timestamp>.mp3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["text"]
|
"required": ["text"]
|
||||||
|
|||||||
Reference in New Issue
Block a user