"""Shared utilities for the memory module — NATS access, path helpers, JSON loading.""" import json import logging import os from datetime import datetime, timezone from pathlib import Path def get_workspace_dir() -> Path: """Get workspace directory from WORKSPACE_DIR env or cwd.""" return Path(os.environ.get("WORKSPACE_DIR", os.getcwd())) def get_agent_name() -> str: """Get agent name from AGENT_NAME env or 'agent'.""" return os.environ.get("AGENT_NAME", "agent") def get_reboot_dir(workspace: Path = None) -> Path: """Get memory/reboot directory, creating if needed.""" ws = workspace or get_workspace_dir() d = ws / "memory" / "reboot" d.mkdir(parents=True, exist_ok=True) return d def get_nats_credentials(workspace: Path = None) -> dict: """Load NATS credentials from env vars or config file. Priority: env vars > config file at WORKSPACE_DIR/config/nats/credentials.env Returns dict with keys: url, user, password """ url = os.environ.get("NATS_URL", "") user = os.environ.get("NATS_USER", "") password = os.environ.get("NATS_PASSWORD", "") if not (url and user and password): ws = workspace or get_workspace_dir() creds_file = ws / "config" / "nats" / "credentials.env" if creds_file.exists(): for line in creds_file.read_text().strip().split("\n"): if "=" in line and not line.startswith("#"): k, v = line.split("=", 1) k, v = k.strip(), v.strip().strip('"') if k == "NATS_URL" and not url: url = v elif k in ("NATS_USER",) and not user: user = v elif k in ("NATS_PASSWORD", "NATS_CLAUDIA_PW") and not password: password = v return { "url": url or "nats://localhost:4222", "user": user, "password": password, } def get_nats_env(workspace: Path = None) -> dict: """Return os.environ copy with NATS credentials set for nats CLI.""" creds = get_nats_credentials(workspace) env = os.environ.copy() if creds["user"]: env["NATS_USER"] = creds["user"] if creds["password"]: env["NATS_PASSWORD"] = creds["password"] if creds["url"]: env["NATS_URL"] = creds["url"] return env def load_json(path: Path) -> dict: """Load JSON file, returning empty dict on failure.""" try: return json.loads(path.read_text()) except (FileNotFoundError, json.JSONDecodeError): return {} def save_json(path: Path, data: dict): """Atomically write JSON to file.""" path.parent.mkdir(parents=True, exist_ok=True) tmp = path.with_suffix(".tmp") tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False)) tmp.rename(path) def load_facts(path: Path) -> list[dict]: """Load facts from a JSONL file.""" if not path.exists(): return [] facts = [] for line in path.read_text().strip().split("\n"): if not line.strip(): continue try: fact = json.loads(line) if "text" not in fact and "fact" in fact: fact["text"] = fact["fact"] facts.append(fact) except json.JSONDecodeError: continue return facts def setup_logging(name: str, workspace: Path = None) -> logging.Logger: """Configure logging to workspace/logs/ and stderr.""" ws = workspace or get_workspace_dir() log_dir = ws / "logs" log_dir.mkdir(parents=True, exist_ok=True) log_file = log_dir / f"{name}.log" logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(log_file), logging.StreamHandler(), ], ) return logging.getLogger(name)