"""Environment configuration for Bolt Console. Same lightweight ".env next to the script" pattern as desk_client/bolt_desk.py in the main tmn-api repo, so this project can be copied anywhere (it does not import anything from that repo) and configured the same way. """ from __future__ import annotations import os import platform import sys from pathlib import Path if getattr(sys, "frozen", False): # PyInstaller build: the .env sits next to the .exe, not inside the bundle. HERE = Path(sys.executable).resolve().parent else: HERE = Path(__file__).resolve().parent.parent # project root (one above bolt_console/) def _load_env() -> None: env_path = HERE / ".env" if not env_path.exists(): return for line in env_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") os.environ.setdefault(key.strip(), value.strip()) _load_env() def _flag(name: str, default: str) -> bool: return os.environ.get(name, default).lower() in ("1", "true", "yes", "on") def _node_name() -> str: try: return platform.node() or "desktop" except Exception: return "desktop" # ── server / identity ─────────────────────────────────────────────────────── # The session id keeps the "pet-" prefix on purpose: the desk API advertises # the `filectl` read/write/edit tool to pet sessions, and this console is # built around that tool. "-console" keeps it a separate conversation from a # Bolt-Pet running on the same machine. SERVER_URL = os.environ.get("BOLT_SERVER_URL", "").rstrip("/") API_KEY = os.environ.get("DESK_API_KEY", "") # # "New chat" (GUI) / /new (terminal) rotates to a fresh id, remembered in # STATE_FILE so a restart carries on the same conversation. DESK_SESSION_ID in # .env pins the session and takes precedence over the remembered one. STATE_FILE = Path.home() / ".cache" / "bolt-console" / "session.json" _BASE_SESSION = "pet-console-" + _node_name() def _remembered_session() -> str: try: import json value = json.loads(STATE_FILE.read_text(encoding="utf-8")).get("session_id") return str(value) if value else "" except Exception: return "" # missing or corrupt state means the default, never a crash SESSION_ID = os.environ.get("DESK_SESSION_ID") or _remembered_session() or _BASE_SESSION def set_session(session_id: str) -> str: """Make *session_id* the current conversation and remember it for the next start. A plain reassignment of the module attribute every server_client call reads; not locked against an in-flight request, which is fine for a rare, deliberate user action (the window blocks switching while a turn is running anyway).""" import json global SESSION_ID SESSION_ID = session_id try: STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps({"session_id": SESSION_ID}), encoding="utf-8") except OSError: pass # switched for this run; it just won't survive a restart return SESSION_ID def new_session() -> str: """A locally made fresh id — the fallback when the server can't mint one (POST /desk/chats is the normal route; see Session.new_conversation).""" import time return set_session(f"{_BASE_SESSION}-{time.strftime('%Y%m%d-%H%M%S')}") def session_pinned() -> bool: return bool(os.environ.get("DESK_SESSION_ID")) # ── relayed commands ──────────────────────────────────────────────────────── COMMAND_TIMEOUT_SECONDS = int(os.environ.get("COMMAND_TIMEOUT_SECONDS", "120")) # What happens when Bolt wants to change something on this machine: # ask — shell commands and file writes/edits wait for y/n at the prompt # (reads and directory listings never ask). # auto — everything runs without asking, like the pet did. # Toggle for the current session with /auto and /ask. APPROVAL_MODE = os.environ.get("APPROVAL_MODE", "ask").strip().lower() # ── replies ───────────────────────────────────────────────────────────────── # Print each sentence as the server produces it instead of waiting out the # whole model call. Falls back to the ordinary request/response path when the # stream fails before anything was printed. STREAMING_REPLIES = _flag("STREAMING_REPLIES", "true") # Append a one-line note (working directory, OS) to each message, so "what's # in this folder?" has a referent and Bolt knows there's no pet body here. CONSOLE_CONTEXT = _flag("CONSOLE_CONTEXT", "true") # ANSI colour. Also off when NO_COLOR is set or stdout isn't a terminal. COLOR = _flag("COLOR", "true") and "NO_COLOR" not in os.environ # ── file delivery ─────────────────────────────────────────────────────────── # The server's deliver_files tool (ai/desk_api.py in the main tmn-api repo) # queues files on this session — e.g. "send me that report" — for the client # to fetch via GET /desk/files. Downloading a file dequeues it server-side, so # each one lands here exactly once. RECEIVE_FILES = _flag("RECEIVE_FILES", "true") DELIVERED_FILES_DIR = Path( os.environ.get("DELIVERED_FILES_DIR") or str(Path.home() / "Downloads" / "Bolt") ).expanduser() # ── dictation (the mic button) ────────────────────────────────────────────── # Speech goes to the server's /desk/stt relay, like Bolt-Pet. MIC_DEVICE is an # input index or name substring (blank = system default); a recording stops # itself after DICTATION_SILENCE_SECONDS of quiet once you've said something. MIC_DEVICE = os.environ.get("MIC_DEVICE", "") DICTATION_SILENCE_SECONDS = float(os.environ.get("DICTATION_SILENCE_SECONDS", "3")) def is_configured() -> bool: return bool(SERVER_URL and API_KEY) def missing_config() -> list: missing = [] if not SERVER_URL: missing.append("BOLT_SERVER_URL") if not API_KEY: missing.append("DESK_API_KEY") return missing