from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from dotenv import load_dotenv def _bool_env(name: str, default: bool) -> bool: raw = os.getenv(name) if raw is None: return default return raw.strip().lower() in {"1", "true", "yes", "y", "on"} def _int_env(name: str, default: int) -> int: raw = os.getenv(name) if not raw: return default return int(raw) def _id_set(name: str) -> set[int]: raw = os.getenv(name, "") ids: set[int] = set() for part in raw.replace(";", ",").split(","): part = part.strip() if not part: continue ids.add(int(part)) return ids @dataclass(frozen=True) class Config: telegram_bot_token: str allowed_user_ids: set[int] state_path: Path workspace_root: Path codex_bin: str codex_timeout_seconds: int codex_backend: str app_server_url: str app_server_model: str | None app_server_reasoning_effort: str | None app_server_speed_mode: str readonly_sandbox: str write_sandbox: str resume_json_flag_style: str allow_plain_text: bool timezone: str schedule_poll_seconds: int persistent_ws_idle_seconds: int gitea_base_url: str | None gitea_username: str | None gitea_email: str | None gitea_default_owner: str | None gitea_default_repo: str | None gitea_token_configured: bool @classmethod def load(cls) -> "Config": load_dotenv() token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip() if not token: raise RuntimeError("TELEGRAM_BOT_TOKEN is required") backend = os.getenv("CODEX_BACKEND", "auto").strip().lower() if backend not in {"auto", "app-server", "exec"}: raise RuntimeError("CODEX_BACKEND must be one of: auto, app-server, exec") app_server_model = os.getenv("CODEX_APP_SERVER_MODEL", "").strip() or "gpt-5.5" app_server_reasoning_effort = ( os.getenv("CODEX_MODEL_REASONING_EFFORT", "").strip() or "high" ) app_server_speed_mode = os.getenv("CODEX_SPEED_MODE", "standard").strip().lower() if app_server_speed_mode not in {"standard", "fast"}: raise RuntimeError("CODEX_SPEED_MODE must be one of: standard, fast") return cls( telegram_bot_token=token, allowed_user_ids=_id_set("ALLOWED_TELEGRAM_USER_IDS"), state_path=Path(os.getenv("BOT_STATE_PATH", "/app/data/state.json")), workspace_root=Path(os.getenv("BOT_WORKSPACE_ROOT", "/workspaces")), codex_bin=os.getenv("CODEX_BIN", "codex"), codex_timeout_seconds=_int_env("CODEX_TIMEOUT_SECONDS", 1800), codex_backend=backend, app_server_url=os.getenv("CODEX_APP_SERVER_URL", "ws://127.0.0.1:4500"), app_server_model=app_server_model, app_server_reasoning_effort=app_server_reasoning_effort, app_server_speed_mode=app_server_speed_mode, readonly_sandbox=os.getenv("CODEX_READONLY_SANDBOX", "read-only"), write_sandbox=os.getenv("CODEX_WRITE_SANDBOX", "workspace-write"), resume_json_flag_style=os.getenv( "CODEX_RESUME_JSON_FLAG_STYLE", "before_resume" ), allow_plain_text=_bool_env("ALLOW_PLAIN_TEXT", True), timezone=os.getenv("BOT_TIMEZONE", "Asia/Seoul"), schedule_poll_seconds=_int_env("SCHEDULE_POLL_SECONDS", 30), persistent_ws_idle_seconds=_int_env("PERSISTENT_WS_IDLE_SECONDS", 600), gitea_base_url=os.getenv("GITEA_BASE_URL", "").strip() or None, gitea_username=os.getenv("GITEA_USERNAME", "").strip() or None, gitea_email=os.getenv("GITEA_EMAIL", "").strip() or None, gitea_default_owner=os.getenv("GITEA_DEFAULT_OWNER", "").strip() or None, gitea_default_repo=os.getenv("GITEA_DEFAULT_REPO", "").strip() or None, gitea_token_configured=bool(os.getenv("GITEA_TOKEN", "").strip()), )