From b94ef01f822e7fcfbb753b205ffcfbd4665559f1 Mon Sep 17 00:00:00 2001 From: Rafael Madriz Date: Fri, 22 May 2026 17:18:06 -0300 Subject: [PATCH] feat(core): add XDG_CONFIG_HOME support (#844) Signed-off-by: Rafael Madriz --- src/basic_memory/config.py | 13 ++++++++----- tests/test_config.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/basic_memory/config.py b/src/basic_memory/config.py index ab06ae73..d567ba3a 100644 --- a/src/basic_memory/config.py +++ b/src/basic_memory/config.py @@ -21,7 +21,7 @@ from basic_memory.utils import setup_logging, generate_permalink DATABASE_NAME = "memory.db" APP_DATABASE_NAME = "memory.db" # Using the same name but in the app directory -DATA_DIR_NAME = ".basic-memory" +DATA_DIR_NAME = "basic-memory" CONFIG_FILE_NAME = "config.json" WATCH_STATUS_JSON = "watch-status.json" CONFIG_DIR_MODE = 0o700 @@ -69,15 +69,18 @@ def resolve_data_dir() -> Path: Single source of truth for the per-user state directory. Honors ``BASIC_MEMORY_CONFIG_DIR`` so each process/worktree can isolate config - and database state; otherwise falls back to ``/.basic-memory``. + and database state; otherwise falls back to ``/.basic-memory``, + and then to ``XDG_CONFIG_HOME``. Cross-platform: ``Path.home()`` reads ``$HOME`` on POSIX and ``%USERPROFILE%`` on Windows, so there's no need to check ``$HOME`` explicitly here. """ - if config_dir := os.getenv("BASIC_MEMORY_CONFIG_DIR"): - return Path(config_dir) - return Path.home() / DATA_DIR_NAME + if basic_memory_dir := os.getenv("BASIC_MEMORY_CONFIG_DIR"): + return Path(basic_memory_dir) + if xdg_config := os.getenv("XDG_CONFIG_HOME"): + return Path(xdg_config) / DATA_DIR_NAME + return Path.home() / ("." + DATA_DIR_NAME) def default_fastembed_cache_dir() -> str: diff --git a/tests/test_config.py b/tests/test_config.py index b07f8621..093d4cf6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -308,8 +308,9 @@ class TestDataDirHelpers: """Module-level helpers that resolve the Basic Memory data directory.""" def test_resolve_data_dir_defaults_to_home_dot_basic_memory(self, config_home, monkeypatch): - """Without BASIC_MEMORY_CONFIG_DIR, resolver returns ~/.basic-memory.""" + """Without BASIC_MEMORY_CONFIG_DIR and XDG_CONFIG_HOME, resolver returns ~/.basic-memory.""" monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) assert resolve_data_dir() == config_home / ".basic-memory" @@ -320,6 +321,23 @@ class TestDataDirHelpers: assert resolve_data_dir() == custom + def test_resolve_data_dir_honors_xdg_config_home(self, tmp_path, monkeypatch): + """XDG_CONFIG_HOME is honored when BASIC_MEMORY_CONFIG_DIR is not set.""" + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) + xdg_config = tmp_path / "xdg-config" + monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg_config)) + + assert resolve_data_dir() == xdg_config / "basic-memory" + + def test_basic_memory_config_dir_takes_precedence_over_xdg(self, tmp_path, monkeypatch): + """BASIC_MEMORY_CONFIG_DIR takes precedence over XDG_CONFIG_HOME.""" + xdg_config = tmp_path / "xdg-config" + custom = tmp_path / "custom-config" + monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg_config)) + monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(custom)) + + assert resolve_data_dir() == custom + def test_default_fastembed_cache_dir_uses_data_dir(self, config_home, monkeypatch): """Default cache path is a subdir of the Basic Memory data dir.""" monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False)