"""Entry point for Matrix bot frontend.""" import asyncio import logging import os import sys from pathlib import Path import httpx import yaml from core.config import Config from core.matrix_bot import MatrixBot def _load_dotenv(workspace: Path) -> None: env_file = workspace / ".env" if not env_file.exists(): return for line in env_file.read_text().splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") key = key.strip() value = value.strip().strip('"').strip("'") if key not in os.environ: os.environ[key] = value def _load_users(workspace: Path) -> dict[str, dict]: """Load users.yml from workspace. Returns {mxid: {profile: ...}}.""" users_file = workspace / "users.yml" if not users_file.exists(): return {} with open(users_file) as f: data = yaml.safe_load(f) or {} return data async def main() -> None: logging.basicConfig( level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) workspace_dir = os.environ.get("WORKSPACE_DIR") if workspace_dir: _load_dotenv(Path(workspace_dir)) # MATRIX_DATA_DIR overrides DATA_DIR for Matrix bot matrix_data_dir = os.environ.get("MATRIX_DATA_DIR") if matrix_data_dir: os.environ["DATA_DIR"] = matrix_data_dir # Matrix-specific env vars homeserver = os.environ.get("MATRIX_HOMESERVER") user_id = os.environ.get("MATRIX_USER_ID") access_token = os.environ.get("MATRIX_ACCESS_TOKEN") owner_mxid = os.environ.get("MATRIX_OWNER_MXID", "") admin_mxid = os.environ.get("MATRIX_ADMIN_MXID", "") # For admin notifications if not all([homeserver, user_id, access_token]): logging.error( "Missing Matrix config. Need: MATRIX_HOMESERVER, MATRIX_USER_ID, " "MATRIX_ACCESS_TOKEN" ) sys.exit(1) # Resolve device_id from server (must match access token) async with httpx.AsyncClient() as http: resp = await http.get( f"{homeserver}/_matrix/client/v3/account/whoami", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) if resp.status_code != 200: logging.error("whoami failed (%d): %s", resp.status_code, resp.text) sys.exit(1) device_id = resp.json().get("device_id") logging.info("Resolved device_id: %s", device_id) # Load users map (multi-user mode) users = {} if workspace_dir: users = _load_users(Path(workspace_dir)) if not users and not owner_mxid: logging.error("Need either users.yml in workspace or MATRIX_OWNER_MXID env var") sys.exit(1) try: config = Config.from_env() except ValueError as e: logging.error("Config error: %s", e) sys.exit(1) if config.workspace_dir: logging.info("Workspace: %s", config.workspace_dir) # Symlink workspace CLAUDE.md into data dir claude_md_link = config.data_dir / "CLAUDE.md" claude_md_src = config.workspace_dir / "CLAUDE.md" if claude_md_src.exists() and not claude_md_link.exists(): claude_md_link.symlink_to(claude_md_src) logging.info("Symlinked CLAUDE.md into data dir") if users: logging.info("Multi-user mode: %d users", len(users)) logging.info("Data dir: %s", config.data_dir) bot = MatrixBot(config, homeserver, user_id, access_token, owner_mxid=owner_mxid, users=users, device_id=device_id, admin_mxid=admin_mxid) try: await bot.run() except KeyboardInterrupt: pass finally: await bot.close() if __name__ == "__main__": asyncio.run(main())