refactor: remove all hardcoded paths, use env vars + config
All checks were successful
Tests / test (push) Successful in 2s

All ~/clawd/ references replaced with configurable paths:
- CORTEX_HOME (default: ~/.cortex)
- CORTEX_MEMORY_DIR, CORTEX_CONFIG, CORTEX_GROWTH_LOG, CORTEX_ROADMAP
- permanent_files configurable via config.json
- Tests pass both with and without env vars set
- 169/169 tests green
This commit is contained in:
Claudia 2026-02-09 12:13:18 +01:00
parent 0972e81ec8
commit 734f96cfcf
9 changed files with 117 additions and 23 deletions

View file

@ -21,6 +21,26 @@ Intelligence layer for [OpenClaw](https://github.com/moltbot/moltbot). The highe
pip install -e . pip install -e .
``` ```
## Configuration
Cortex uses environment variables for path configuration. All paths have sensible defaults (`~/.cortex/`).
| Variable | Default | Description |
|----------|---------|-------------|
| `CORTEX_HOME` | `~/.cortex` | Base directory |
| `CORTEX_MEMORY_DIR` | `$CORTEX_HOME/memory` | Memory files |
| `CORTEX_CONFIG` | `$CORTEX_HOME/config.json` | Config file |
| `CORTEX_GROWTH_LOG` | `$CORTEX_MEMORY_DIR/growth-log.md` | Feedback loop output |
| `CORTEX_ROADMAP` | `$CORTEX_MEMORY_DIR/roadmap.json` | Roadmap data |
Optional `config.json` for customization:
```json
{
"permanent_files": ["MEMORY.md", "WORKING.md", "README.md"],
"sessions_dir": "~/.openclaw/agents/main/sessions"
}
```
## Docker ## Docker
```bash ```bash

View file

@ -8,7 +8,7 @@ Usage:
cortex hygiene stats|stale|duplicates|orphans|archive cortex hygiene stats|stale|duplicates|orphans|archive
cortex roadmap list|add|update|overdue|upcoming|report cortex roadmap list|add|update|overdue|upcoming|report
cortex validate --transcript <path> --task "description" cortex validate --transcript <path> --task "description"
cortex search "query" [--memory-dir ~/clawd/memory] cortex search "query" [--memory-dir ~/.cortex/memory]
cortex handoff --from <session> --to <agent> --task "description" cortex handoff --from <session> --to <agent> --task "description"
cortex version cortex version
""" """

72
cortex/config.py Normal file
View file

@ -0,0 +1,72 @@
"""Cortex configuration — all paths and settings resolved here.
Priority: CLI args > environment variables > config file > defaults.
Environment variables:
CORTEX_HOME Base directory (default: ~/.cortex)
CORTEX_MEMORY_DIR Memory files directory
CORTEX_CONFIG Path to config.json
CORTEX_GROWTH_LOG Path to growth log file
CORTEX_ROADMAP Path to roadmap.json
"""
import json
import os
from pathlib import Path
from typing import Any, Optional
def _env(key: str, default: str | None = None) -> str | None:
return os.environ.get(key, default)
def cortex_home() -> Path:
"""Base directory for cortex data."""
return Path(_env("CORTEX_HOME", str(Path.home() / ".cortex")))
def memory_dir() -> Path:
return Path(_env("CORTEX_MEMORY_DIR", str(cortex_home() / "memory")))
def growth_log() -> Path:
return Path(_env("CORTEX_GROWTH_LOG", str(memory_dir() / "growth-log.md")))
def roadmap_file() -> Path:
return Path(_env("CORTEX_ROADMAP", str(memory_dir() / "roadmap.json")))
def archive_dir() -> Path:
return memory_dir() / "archive"
def logs_dir() -> Path:
return cortex_home() / "logs"
def config_path() -> Path:
return Path(_env("CORTEX_CONFIG", str(cortex_home() / "config.json")))
def load_config(path: Optional[Path] = None) -> dict[str, Any]:
"""Load config.json. Returns empty dict if not found."""
p = path or config_path()
if p.exists():
return json.loads(p.read_text())
return {}
# Default permanent files — overridable via config.json "permanent_files" key
DEFAULT_PERMANENT_FILES = {
"README.md",
}
def permanent_files(config: Optional[dict] = None) -> set[str]:
"""Files that should never be archived or deleted."""
cfg = config or load_config()
custom = cfg.get("permanent_files", [])
if custom:
return set(custom)
return DEFAULT_PERMANENT_FILES

View file

@ -30,14 +30,12 @@ from typing import Optional
from cortex.composite_scorer import SearchResult, score_results, load_config as load_scorer_config from cortex.composite_scorer import SearchResult, score_results, load_config as load_scorer_config
from cortex.intent_classifier import classify, IntentResult from cortex.intent_classifier import classify, IntentResult
UNIFIED_MEMORY_SCRIPT = Path.home() / "clawd" / "scripts" / "unified-memory.py" UNIFIED_MEMORY_SCRIPT = Path(os.environ.get("CORTEX_UNIFIED_MEMORY_SCRIPT", str(Path.home() / ".cortex" / "scripts" / "unified-memory.py")))
PYTHON = sys.executable or "/usr/bin/python3" PYTHON = sys.executable or "/usr/bin/python3"
# Paths to search directly if unified-memory.py is unavailable # Paths to search directly if unified-memory.py is unavailable
SEARCH_PATHS = [ SEARCH_PATHS = [
Path.home() / "clawd" / "memory", Path(os.environ.get("CORTEX_MEMORY_DIR", str(Path.home() / ".cortex" / "memory"))),
Path.home() / "clawd" / "companies",
Path.home() / "clawd" / "MEMORY.md",
Path.home() / "life" / "areas", Path.home() / "life" / "areas",
] ]

View file

@ -323,7 +323,8 @@ def append_to_growth_log(findings: list[Finding], config: dict, dry_run: bool =
text += finding_to_markdown(f, config) + "\n" text += finding_to_markdown(f, config) + "\n"
if dry_run: if dry_run:
return text return text
log_path = expand(config.get("growth_log_path", "~/clawd/memory/growth-log.md")) from cortex.config import growth_log
log_path = expand(config.get("growth_log_path", str(growth_log())))
with open(log_path, "a") as fh: with open(log_path, "a") as fh:
fh.write(text) fh.write(text)
return text return text

View file

@ -228,7 +228,7 @@ class HealthScanner:
"""Check for large SQLite/DB files.""" """Check for large SQLite/DB files."""
search_paths = [ search_paths = [
Path.home() / ".openclaw", Path.home() / ".openclaw",
Path.home() / "clawd", Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))),
] ]
for base in search_paths: for base in search_paths:
if not base.exists(): if not base.exists():
@ -250,7 +250,7 @@ class HealthScanner:
def _check_log_sizes(self): def _check_log_sizes(self):
log_dirs = [ log_dirs = [
Path.home() / ".openclaw" / "logs", Path.home() / ".openclaw" / "logs",
Path.home() / "clawd" / "logs", Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))) / "logs",
Path("/tmp"), Path("/tmp"),
] ]
for d in log_dirs: for d in log_dirs:
@ -320,16 +320,16 @@ class HealthScanner:
def _check_chromadb(self): def _check_chromadb(self):
rag_dirs = list(Path.home().glob("**/.rag-db"))[:3] rag_dirs = list(Path.home().glob("**/.rag-db"))[:3]
clawd_rag = Path.home() / "clawd" / ".rag-db" rag_db = Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))) / ".rag-db"
if clawd_rag.exists(): if rag_db.exists():
age_hours = (time.time() - clawd_rag.stat().st_mtime) / 3600 age_hours = (time.time() - rag_db.stat().st_mtime) / 3600
if age_hours > 48: if age_hours > 48:
self._add("deps", WARN, self._add("deps", WARN,
f"ChromaDB (.rag-db) last modified {age_hours:.0f}h ago") f"ChromaDB (.rag-db) last modified {age_hours:.0f}h ago")
else: else:
self._add("deps", INFO, f"ChromaDB (.rag-db) fresh ({age_hours:.0f}h)") self._add("deps", INFO, f"ChromaDB (.rag-db) fresh ({age_hours:.0f}h)")
else: else:
self._add("deps", INFO, "No .rag-db found in clawd workspace") self._add("deps", INFO, "No .rag-db found in workspace")
def _check_ollama(self): def _check_ollama(self):
try: try:
@ -344,7 +344,7 @@ class HealthScanner:
def _check_key_expiry(self): def _check_key_expiry(self):
"""Scan env files for date-like patterns that might indicate key expiry.""" """Scan env files for date-like patterns that might indicate key expiry."""
env_files = list(Path.home().glob(".config/**/*.env")) env_files = list(Path.home().glob(".config/**/*.env"))
env_files += list(Path.home().glob("clawd/**/.env")) env_files += list(Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))).glob("**/.env"))
env_files += [Path.home() / ".env"] env_files += [Path.home() / ".env"]
now = datetime.now() now = datetime.now()
date_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})') date_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})')

View file

@ -12,15 +12,12 @@ from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
MEMORY_DIR = Path.home() / "clawd" / "memory" from cortex.config import memory_dir, archive_dir, permanent_files as get_permanent_files
ARCHIVE_DIR = MEMORY_DIR / "archive" MEMORY_DIR = memory_dir()
ARCHIVE_DIR = archive_dir()
CONFIG_PATH = Path(__file__).parent / "config.json" CONFIG_PATH = Path(__file__).parent / "config.json"
PERMANENT_FILES = { PERMANENT_FILES = get_permanent_files()
"MEMORY.md", "WORKING.md", "growth-log.md", "BOOT_CONTEXT.md",
"README.md", "active-context.json", "network-map.md",
"learned-context.md", "email-contacts.json",
}
DAILY_NOTE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}(?:-.+)?\.md$") DAILY_NOTE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}(?:-.+)?\.md$")
DATE_RE = re.compile(r"\b(20\d{2})-(\d{2})-(\d{2})\b") DATE_RE = re.compile(r"\b(20\d{2})-(\d{2})-(\d{2})\b")

View file

@ -19,7 +19,8 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
ROADMAP_FILE = Path.home() / "clawd" / "memory" / "roadmap.json" from cortex.config import roadmap_file
ROADMAP_FILE = roadmap_file()
VALID_STATUSES = {"open", "in_progress", "blocked", "done"} VALID_STATUSES = {"open", "in_progress", "blocked", "done"}
VALID_PRIORITIES = {"P0", "P1", "P2", "P3"} VALID_PRIORITIES = {"P0", "P1", "P2", "P3"}

View file

@ -272,8 +272,13 @@ class TestMemoryOrphans(unittest.TestCase):
def test_permanent_files_not_orphaned(self): def test_permanent_files_not_orphaned(self):
(self.tmpdir / "WORKING.md").write_text("Current work") (self.tmpdir / "WORKING.md").write_text("Current work")
orph = mh.find_orphans() orig = mh.PERMANENT_FILES
self.assertNotIn("WORKING.md", orph["orphaned_files"]) mh.PERMANENT_FILES = {"WORKING.md", "README.md"}
try:
orph = mh.find_orphans()
self.assertNotIn("WORKING.md", orph["orphaned_files"])
finally:
mh.PERMANENT_FILES = orig
class TestMemoryStats(unittest.TestCase): class TestMemoryStats(unittest.TestCase):