diff --git a/CHANGELOG.md b/CHANGELOG.md index b987dc64..42919130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) + +### Fixed + +- **PR #2897** by @chouzz — On Windows, WebUI default state and config paths now align with Hermes Agent's `%LOCALAPPDATA%\hermes` convention instead of `%USERPROFILE%\.hermes`, so a fresh Windows install finds the same `~/.hermes/config.yaml` / `auth.json` / `webui/` state directory that the Hermes Agent created. POSIX behavior is unchanged (`~/.hermes` remains the default). `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` env overrides take precedence on both platforms. Closes #2840. + ## [v0.51.133] — 2026-05-25 — Release DE (stage-batch15 — 6-PR contributor batch — aux-task validation + workspace artifact gating + update apply guard + Joplin auth header + prefill cache guard + notes drawer i18n) ### Fixed diff --git a/README.md b/README.md index 9ba22ee4..26f18946 100644 --- a/README.md +++ b/README.md @@ -241,9 +241,9 @@ For the deep dive on each of these, see [`docs/docker.md`](docs/docker.md). | Thing | How it finds it | |---|---| -| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `~/.hermes/hermes-agent`, then sibling `../hermes-agent` | +| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent` (Windows default `%LOCALAPPDATA%\hermes\hermes-agent`, POSIX default `~/.hermes/hermes-agent`), then sibling `../hermes-agent` | | Python executable | Agent venv first, then `.venv` in this repo, then system `python3` | -| State directory | `HERMES_WEBUI_STATE_DIR` env, then `~/.hermes/webui` | +| State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | | Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir | | Port | `HERMES_WEBUI_PORT` env or first argument, default `8787` | @@ -275,15 +275,15 @@ Full list of environment variables: | `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable | | `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) | | `HERMES_WEBUI_PORT` | `8787` | Port | -| `HERMES_WEBUI_STATE_DIR` | `~/.hermes/webui` | Where sessions and state are stored | +| `HERMES_WEBUI_STATE_DIR` | `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | Where sessions and state are stored | | `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace | | `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default | | `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication | | `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled | | `HERMES_WEBUI_EXTENSION_SCRIPT_URLS` | *(unset)* | Optional comma-separated same-origin script URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) | | `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS` | *(unset)* | Optional comma-separated same-origin stylesheet URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) | -| `HERMES_HOME` | `~/.hermes` | Base directory for Hermes state (affects all paths) | -| `HERMES_CONFIG_PATH` | `~/.hermes/config.yaml` | Path to Hermes config file | +| `HERMES_HOME` | Windows: `%LOCALAPPDATA%\hermes`; POSIX: `~/.hermes` | Base directory for Hermes state (affects all paths) | +| `HERMES_CONFIG_PATH` | `$HERMES_HOME/config.yaml` | Path to Hermes config file | --- diff --git a/api/config.py b/api/config.py index d49d9120..1e679ed5 100644 --- a/api/config.py +++ b/api/config.py @@ -30,6 +30,19 @@ HOME = Path.home() # REPO_ROOT is the directory that contains this file's parent (api/ -> repo root) REPO_ROOT = Path(__file__).parent.parent.resolve() + +def _platform_default_hermes_home() -> Path: + """Return the platform-aware default Hermes home when HERMES_HOME is unset. + + Native Windows Hermes Agent installs default to %LOCALAPPDATA%\\hermes, + while POSIX installs use ~/.hermes. + """ + if os.name == "nt": + local_app_data = os.getenv("LOCALAPPDATA", "").strip() + if local_app_data: + return Path(local_app_data) / "hermes" + return HOME / ".hermes" + # ── Network config (env-overridable) ───────────────────────────────────────── HOST = os.getenv("HERMES_WEBUI_HOST", "127.0.0.1") PORT = int(os.getenv("HERMES_WEBUI_PORT", "8787")) @@ -40,8 +53,10 @@ TLS_KEY = os.getenv("HERMES_WEBUI_TLS_KEY", "").strip() or None TLS_ENABLED = TLS_CERT is not None and TLS_KEY is not None # ── State directory (env-overridable, never inside repo) ────────────────────── +_DEFAULT_HERMES_HOME = _platform_default_hermes_home() + STATE_DIR = ( - Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(HOME / ".hermes" / "webui"))) + Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(_DEFAULT_HERMES_HOME / "webui"))) .expanduser() .resolve() ) @@ -108,7 +123,7 @@ def _discover_agent_dir() -> Path: ) # 2. HERMES_HOME / hermes-agent - hermes_home = os.getenv("HERMES_HOME", str(HOME / ".hermes")) + hermes_home = os.getenv("HERMES_HOME", str(_DEFAULT_HERMES_HOME)) candidates.append(Path(hermes_home).expanduser() / "hermes-agent") # 3. Sibling: /../hermes-agent @@ -119,7 +134,7 @@ def _discover_agent_dir() -> Path: candidates.append(REPO_ROOT.parent) # 5. ~/.hermes/hermes-agent (explicit common path) - candidates.append(HOME / ".hermes" / "hermes-agent") + candidates.append(_DEFAULT_HERMES_HOME / "hermes-agent") # 6. ~/hermes-agent candidates.append(HOME / "hermes-agent") @@ -267,7 +282,7 @@ def _get_config_path() -> Path: return get_active_hermes_home() / "config.yaml" except ImportError: - return HOME / ".hermes" / "config.yaml" + return _DEFAULT_HERMES_HOME / "config.yaml" _WEBUI_SESSION_SAVE_MODES = {"deferred", "eager"} @@ -2368,7 +2383,7 @@ def _get_auth_store_path() -> Path: return _gah() / "auth.json" except ImportError: - return HOME / ".hermes" / "auth.json" + return _DEFAULT_HERMES_HOME / "auth.json" def _models_cache_file_fingerprint(path: Path) -> dict: @@ -3100,7 +3115,7 @@ def get_available_models() -> dict: hermes_env_path = _gah2() / ".env" except ImportError: - hermes_env_path = HOME / ".hermes" / ".env" + hermes_env_path = _DEFAULT_HERMES_HOME / ".env" env_keys = {} if hermes_env_path.exists(): try: diff --git a/api/profiles.py b/api/profiles.py index d11c955c..5220e261 100644 --- a/api/profiles.py +++ b/api/profiles.py @@ -150,6 +150,10 @@ def _resolve_base_hermes_home() -> Path: # If HERMES_HOME points to a profiles/ subdir, walk up two levels to the base return _unwrap_profile_home_to_base(p) + if os.name == 'nt': + local_app_data = os.getenv('LOCALAPPDATA', '').strip() + if local_app_data: + return Path(local_app_data) / 'hermes' return Path.home() / '.hermes' _DEFAULT_HERMES_HOME = _resolve_base_hermes_home() diff --git a/docs/onboarding.md b/docs/onboarding.md index b53b68fc..cedd06cd 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -162,8 +162,8 @@ The wizard uses the same files and APIs as the normal app: State normally lives outside the repository. By default: -- Hermes Agent state: `~/.hermes` -- WebUI state: `~/.hermes/webui` +- Hermes Agent state: Windows `%LOCALAPPDATA%\hermes`; POSIX `~/.hermes` +- WebUI state: `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) Override these with `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` when you need an isolated test install. diff --git a/start.ps1 b/start.ps1 index a8aeb2d4..dac7a406 100644 --- a/start.ps1 +++ b/start.ps1 @@ -159,11 +159,15 @@ $PortFinal = if ($Port) { } $env:HERMES_WEBUI_HOST = $BindHostFinal $env:HERMES_WEBUI_PORT = "$PortFinal" -if (-not $env:HERMES_WEBUI_STATE_DIR) { - $env:HERMES_WEBUI_STATE_DIR = Join-Path $env:USERPROFILE '.hermes\webui' -} if (-not $env:HERMES_HOME) { - $env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes' + if ($env:LOCALAPPDATA) { + $env:HERMES_HOME = Join-Path $env:LOCALAPPDATA 'hermes' + } else { + $env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes' + } +} +if (-not $env:HERMES_WEBUI_STATE_DIR) { + $env:HERMES_WEBUI_STATE_DIR = Join-Path $env:HERMES_HOME 'webui' } # === Ensure dirs exist ================================================= diff --git a/tests/test_issue2840_windows_hermes_home_defaults.py b/tests/test_issue2840_windows_hermes_home_defaults.py new file mode 100644 index 00000000..4d1fd541 --- /dev/null +++ b/tests/test_issue2840_windows_hermes_home_defaults.py @@ -0,0 +1,15 @@ +from pathlib import Path + +import api.config as config +import api.profiles as profiles + + +def test_profiles_unwrap_profile_home_to_base(): + base = Path('/tmp/hermes-base') + profile_home = base / 'profiles' / 'webui' + assert profiles._unwrap_profile_home_to_base(profile_home) == base + + +def test_default_hermes_home_returns_path_object(): + home = config._platform_default_hermes_home() + assert isinstance(home, Path)