diff --git a/README.md b/README.md index 254d96a..4d3dabf 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,26 @@ Intelligence layer for [OpenClaw](https://github.com/moltbot/moltbot). The highe 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 ```bash diff --git a/cortex/cli.py b/cortex/cli.py index ff657a6..5d6e3f5 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -8,7 +8,7 @@ Usage: cortex hygiene stats|stale|duplicates|orphans|archive cortex roadmap list|add|update|overdue|upcoming|report cortex validate --transcript --task "description" - cortex search "query" [--memory-dir ~/clawd/memory] + cortex search "query" [--memory-dir ~/.cortex/memory] cortex handoff --from --to --task "description" cortex version """ diff --git a/cortex/config.py b/cortex/config.py new file mode 100644 index 0000000..f48c3de --- /dev/null +++ b/cortex/config.py @@ -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 diff --git a/cortex/enhanced_search.py b/cortex/enhanced_search.py index 0dfb30f..e5bb24b 100755 --- a/cortex/enhanced_search.py +++ b/cortex/enhanced_search.py @@ -30,14 +30,12 @@ from typing import Optional from cortex.composite_scorer import SearchResult, score_results, load_config as load_scorer_config 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" # Paths to search directly if unified-memory.py is unavailable SEARCH_PATHS = [ - Path.home() / "clawd" / "memory", - Path.home() / "clawd" / "companies", - Path.home() / "clawd" / "MEMORY.md", + Path(os.environ.get("CORTEX_MEMORY_DIR", str(Path.home() / ".cortex" / "memory"))), Path.home() / "life" / "areas", ] diff --git a/cortex/feedback_loop.py b/cortex/feedback_loop.py index 44e322e..727e42f 100644 --- a/cortex/feedback_loop.py +++ b/cortex/feedback_loop.py @@ -323,7 +323,8 @@ def append_to_growth_log(findings: list[Finding], config: dict, dry_run: bool = text += finding_to_markdown(f, config) + "\n" if dry_run: 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: fh.write(text) return text diff --git a/cortex/health_scanner.py b/cortex/health_scanner.py index 454d627..953ad15 100644 --- a/cortex/health_scanner.py +++ b/cortex/health_scanner.py @@ -228,7 +228,7 @@ class HealthScanner: """Check for large SQLite/DB files.""" search_paths = [ Path.home() / ".openclaw", - Path.home() / "clawd", + Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))), ] for base in search_paths: if not base.exists(): @@ -250,7 +250,7 @@ class HealthScanner: def _check_log_sizes(self): log_dirs = [ Path.home() / ".openclaw" / "logs", - Path.home() / "clawd" / "logs", + Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))) / "logs", Path("/tmp"), ] for d in log_dirs: @@ -320,16 +320,16 @@ class HealthScanner: def _check_chromadb(self): rag_dirs = list(Path.home().glob("**/.rag-db"))[:3] - clawd_rag = Path.home() / "clawd" / ".rag-db" - if clawd_rag.exists(): - age_hours = (time.time() - clawd_rag.stat().st_mtime) / 3600 + rag_db = Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))) / ".rag-db" + if rag_db.exists(): + age_hours = (time.time() - rag_db.stat().st_mtime) / 3600 if age_hours > 48: self._add("deps", WARN, f"ChromaDB (.rag-db) last modified {age_hours:.0f}h ago") else: self._add("deps", INFO, f"ChromaDB (.rag-db) fresh ({age_hours:.0f}h)") 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): try: @@ -344,7 +344,7 @@ class HealthScanner: def _check_key_expiry(self): """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("clawd/**/.env")) + env_files += list(Path(os.environ.get("CORTEX_HOME", str(Path.home() / ".cortex"))).glob("**/.env")) env_files += [Path.home() / ".env"] now = datetime.now() date_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})') diff --git a/cortex/memory_hygiene.py b/cortex/memory_hygiene.py index 2480335..7776833 100644 --- a/cortex/memory_hygiene.py +++ b/cortex/memory_hygiene.py @@ -12,15 +12,12 @@ from collections import defaultdict from datetime import datetime, timedelta from pathlib import Path -MEMORY_DIR = Path.home() / "clawd" / "memory" -ARCHIVE_DIR = MEMORY_DIR / "archive" +from cortex.config import memory_dir, archive_dir, permanent_files as get_permanent_files +MEMORY_DIR = memory_dir() +ARCHIVE_DIR = archive_dir() CONFIG_PATH = Path(__file__).parent / "config.json" -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", -} +PERMANENT_FILES = get_permanent_files() 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") diff --git a/cortex/roadmap.py b/cortex/roadmap.py index 4ddf63c..7e07989 100644 --- a/cortex/roadmap.py +++ b/cortex/roadmap.py @@ -19,7 +19,8 @@ from datetime import datetime, timedelta from pathlib import Path 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_PRIORITIES = {"P0", "P1", "P2", "P3"} diff --git a/tests/test_intelligence.py b/tests/test_intelligence.py index 656c7d8..64930fd 100644 --- a/tests/test_intelligence.py +++ b/tests/test_intelligence.py @@ -272,8 +272,13 @@ class TestMemoryOrphans(unittest.TestCase): def test_permanent_files_not_orphaned(self): (self.tmpdir / "WORKING.md").write_text("Current work") - orph = mh.find_orphans() - self.assertNotIn("WORKING.md", orph["orphaned_files"]) + orig = mh.PERMANENT_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):