import secrets import shutil from pathlib import Path import threading # Claude Code conversation directory CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects" # Codex conversation directory CODEX_SESSIONS_DIR = Path.home() / ".codex" / "sessions" # Plugin path for zellij-send-keys ZELLIJ_PLUGIN = Path.home() / ".config" / "zellij" / "plugins" / "zellij-send-keys.wasm" def _resolve_zellij_bin(): """Resolve zellij binary even when PATH is minimal (eg launchctl).""" from_path = shutil.which("zellij") if from_path: return from_path common_paths = ( "/opt/homebrew/bin/zellij", # Apple Silicon Homebrew "/usr/local/bin/zellij", # Intel Homebrew "/usr/bin/zellij", ) for candidate in common_paths: p = Path(candidate) if p.exists() and p.is_file(): return str(p) return "zellij" # Fallback for explicit error reporting by subprocess ZELLIJ_BIN = _resolve_zellij_bin() # Runtime data lives in XDG data dir DATA_DIR = Path.home() / ".local" / "share" / "amc" SESSIONS_DIR = DATA_DIR / "sessions" EVENTS_DIR = DATA_DIR / "events" # Source files live in project directory (relative to this module) PROJECT_DIR = Path(__file__).resolve().parent.parent DASHBOARD_DIR = PROJECT_DIR / "dashboard" PORT = 7400 STALE_EVENT_AGE = 86400 # 24 hours in seconds STALE_STARTING_AGE = 3600 # 1 hour - sessions stuck in "starting" are orphans CODEX_ACTIVE_WINDOW = 600 # 10 minutes - only discover recently-active Codex sessions # Cache for Zellij session list (avoid calling zellij on every request) _zellij_cache = {"sessions": None, "expires": 0} # Cache for Codex pane info (avoid running pgrep/ps/lsof on every request) _codex_pane_cache = {"pid_info": {}, "cwd_map": {}, "expires": 0} # Cache for parsed context usage by transcript file path + mtime/size # Limited to prevent unbounded memory growth _context_usage_cache = {} _CONTEXT_CACHE_MAX = 100 # Cache mapping Codex session IDs to transcript paths (or None when missing) _codex_transcript_cache = {} _CODEX_CACHE_MAX = 200 # Codex sessions dismissed during this server lifetime (prevents re-discovery) # Uses dict (not set) for O(1) lookup + FIFO eviction via insertion order (Python 3.7+) _dismissed_codex_ids = {} _DISMISSED_MAX = 500 # Serialize state collection because it mutates session files/caches. _state_lock = threading.Lock() # Projects directory for spawning agents PROJECTS_DIR = Path.home() / 'projects' # Default Zellij session for spawning ZELLIJ_SESSION = 'infra' # Lock for serializing spawn operations (prevents Zellij race conditions) _spawn_lock = threading.Lock() # Rate limiting: track last spawn time per project (prevents spam) _spawn_timestamps: dict[str, float] = {} SPAWN_COOLDOWN_SEC = 10.0 # Auth token for spawn endpoint _auth_token: str = '' def generate_auth_token(): """Generate a one-time auth token for this server instance.""" global _auth_token _auth_token = secrets.token_urlsafe(32) return _auth_token def validate_auth_token(request_token: str) -> bool: """Validate the Authorization header token.""" return request_token == f'Bearer {_auth_token}' def start_projects_watcher(): """Start background thread to refresh projects cache every 5 minutes.""" import logging from amc_server.mixins.spawn import load_projects_cache def _watch_loop(): import time while True: try: time.sleep(300) load_projects_cache() except Exception: logging.exception('Projects cache refresh failed') thread = threading.Thread(target=_watch_loop, daemon=True) thread.start()