Source code for klea_utils.paths

#!/usr/bin/env python3
"""
Shared user-level directory utilities for Klea packages.

Wraps ``platformdirs.PlatformDirs`` to provide OS-appropriate paths for
cache, data, and config directories (``~/.cache/klea/``, ``~/.local/share/klea/``,
``~/.config/klea/`` on Linux, with equivalents on macOS and Windows).

Consumers pass an *app_name* so that different packages (``klea``, ``nml_mcp``)
get isolated directories without repeating the boilerplate.

File: klea_utils/paths.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import logging
import os
from collections.abc import Mapping
from pathlib import Path

from platformdirs import PlatformDirs

logger = logging.getLogger(__name__)


[docs] def get_cache_dir(dirs: PlatformDirs) -> Path: """Return the OS-appropriate per-user cache directory for *dirs*. On Linux: ``~/.cache/{app_name}/`` On macOS: ``~/Library/Caches/{app_name}/`` On Windows: ``C:\\Users\\<user>\\AppData\\Local\\{app_name}\\cache\\`` """ return Path(dirs.user_cache_dir)
[docs] def get_data_dir(dirs: PlatformDirs) -> Path: """Return the OS-appropriate per-user data directory for *dirs*. On Linux: ``~/.local/share/{app_name}/`` On macOS: ``~/Library/Application Support/{app_name}/`` On Windows: ``C:\\Users\\<user>\\AppData\\Local\\{app_name}\\`` """ return Path(dirs.user_data_dir)
[docs] def get_config_dir(dirs: PlatformDirs) -> Path: """Return the OS-appropriate per-user config directory for *dirs*. On Linux: ``~/.config/{app_name}/`` On macOS: ``~/Library/Preferences/{app_name}/`` On Windows: ``C:\\Users\\<user>\\AppData\\Roaming\\{app_name}\\`` """ return Path(dirs.user_config_dir)
[docs] def resolve_app_config_path( config_file: str, conf_dir: str | Path, cwd: str | Path | None = None, ) -> Path: """Locate the application config file, checking the working directory first. ``config_file`` may be an absolute path or a relative name/path. The working directory is searched before *conf_dir* (the per-app config directory), so a profile dropped in ``CWD`` overrides one installed in the config directory. :param config_file: App config file path or bare filename :param conf_dir: Config directory searched after the working directory :param cwd: Working directory to search first (defaults to ``Path.cwd()``) :returns: The first existing match :raises FileNotFoundError: If no existing file matches :raises ValueError: If *config_file* is empty """ if not config_file: raise ValueError("Empty config file name") cwd_path = Path(cwd) if cwd is not None else Path.cwd() conf_dir_path = Path(conf_dir) candidate = Path(config_file) if candidate.is_absolute(): if candidate.exists(): return candidate raise FileNotFoundError(f"Could not find config file: {candidate}") cwd_candidate = cwd_path / candidate if cwd_candidate.exists(): return cwd_candidate conf_candidate = conf_dir_path / candidate if conf_candidate.exists(): return conf_candidate raise FileNotFoundError( f"Could not find config file '{config_file}' in:\n" f" {cwd_path}\n" f" {conf_dir_path}\n" "Create it there, or select another config with --profile <name>." )
[docs] def init_dir(path: str | Path) -> Path: """Create *path* (and parents) if it doesn't exist. :returns: The same *path* as a ``Path`` for chaining. """ p = Path(path) p.mkdir(parents=True, exist_ok=True) return p
#: Application name used to derive the default per-user data directory. The #: ``klea_agent`` data directory is the single *storage root* that owns the #: checkpoint database, the session database, the backend PID lockfile, the #: endpoint file and the logs (ADR-0023, amendment "one backend per storage #: root"). STORAGE_APP_NAME = "klea_agent" #: Environment variable that relocates the whole storage root -- data *and* #: runtime artifacts -- so a user can run deliberately independent instances. #: It is an *isolation* override, not a concurrency mechanism (ADR-0023). STORAGE_ROOT_ENV = "KLEA_AGENT_HOME" #: Name of the SQLite database that holds LangGraph checkpoints. CHECKPOINT_DB_NAME = "checkpoints.db" #: Name of the SQLite database that holds chat sessions and messages. SESSION_DB_NAME = "sessions.db" #: Name of the PID lockfile that enforces one backend per storage root. LOCKFILE_NAME = "klea_agent.lock" #: Name of the file a running backend writes its endpoint (host:port) to. ENDPOINT_FILE_NAME = "klea_agent.endpoint" #: Subdirectory of the storage root that holds backend logs. LOG_DIR_NAME = "logs"
[docs] def resolve_storage_root( data_dir: str | Path | None = None, env: Mapping[str, str] | None = None, *, default_root: str | Path | None = None, env_name: str | None = STORAGE_ROOT_ENV, ) -> Path: """Return the effective storage root for a Klea backend. Precedence, highest first: #. *data_dir* -- the ``--data-dir`` flag, i.e. an explicit choice #. *env_name* (default :data:`STORAGE_ROOT_ENV`, ``KLEA_AGENT_HOME``) -- an opt-in relocation of the data *and* runtime artifacts #. *default_root* -- the graph's existing per-app data directory, when given #. the platform user data directory for :data:`STORAGE_APP_NAME` (``~/.local/share/klea_agent`` on Linux) The resolved root is logged at ``INFO`` level together with the source that selected it, so a user can tell which home a running backend uses. :param data_dir: Value of the ``--data-dir`` flag, if given :param env: Environment mapping to read (defaults to :data:`os.environ`) :param default_root: Per-app data directory used when no relocation is configured; preserves a graph's existing default layout :param env_name: Environment variable to read, or ``None`` to ignore it :returns: The resolved storage root, absolute and expanded """ environ = os.environ if env is None else env if data_dir: root = Path(data_dir).expanduser() source = "the --data-dir flag" else: from_env = environ.get(env_name) if env_name else None if from_env: root = Path(from_env).expanduser() source = f"{env_name}={from_env!r}" elif default_root is not None: root = Path(default_root).expanduser() source = "the default user data dir" else: root = get_data_dir(PlatformDirs(STORAGE_APP_NAME)) source = f"the default user data dir for {STORAGE_APP_NAME!r}" root = root.resolve() logger.info(f"Resolved storage root: {root} (from {source})") return root
def _storage_root(root: str | Path | None) -> Path: """Return *root* as a :class:`Path`, resolving it when not supplied.""" if root is None: return resolve_storage_root() return Path(root).expanduser()
[docs] def checkpoint_db_path(root: str | Path | None = None) -> Path: """Return the LangGraph checkpoint database path inside *root*. :param root: Storage root; resolved via :func:`resolve_storage_root` when ``None`` """ return _storage_root(root) / CHECKPOINT_DB_NAME
[docs] def session_db_path(root: str | Path | None = None) -> Path: """Return the chat session/message database path inside *root*. :param root: Storage root; resolved via :func:`resolve_storage_root` when ``None`` """ return _storage_root(root) / SESSION_DB_NAME
[docs] def lockfile_path(root: str | Path | None = None) -> Path: """Return the backend PID lockfile path inside *root*. :param root: Storage root; resolved via :func:`resolve_storage_root` when ``None`` """ return _storage_root(root) / LOCKFILE_NAME
[docs] def endpoint_file_path(root: str | Path | None = None) -> Path: """Return the path of the file that advertises a backend's endpoint. :param root: Storage root; resolved via :func:`resolve_storage_root` when ``None`` """ return _storage_root(root) / ENDPOINT_FILE_NAME
[docs] def log_dir_path(root: str | Path | None = None) -> Path: """Return the log directory inside *root*. The directory is only *named*, never created: callers use :func:`init_dir` when they are ready to write logs. :param root: Storage root; resolved via :func:`resolve_storage_root` when ``None`` """ return _storage_root(root) / LOG_DIR_NAME
[docs] def cleanup_dir(path: str | Path) -> None: """Remove all *contents* of *path* but keep the directory itself.""" p = Path(path) if not p.exists(): return import shutil for item in p.iterdir(): if item.is_file() or item.is_symlink(): item.unlink() elif item.is_dir(): shutil.rmtree(item)