refactor: remove all hardcoded paths, use env vars + config
All checks were successful
Tests / test (push) Successful in 2s
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:
parent
0972e81ec8
commit
734f96cfcf
9 changed files with 117 additions and 23 deletions
20
README.md
20
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
72
cortex/config.py
Normal 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
|
||||||
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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})')
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue