feat(telegram): load config from telegram.toml
This commit is contained in:
@@ -7,16 +7,48 @@ import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
TELEGRAM_HARD_LIMIT = 4096
|
||||
DEFAULT_CHUNK_LEN = 3500 # leave room for formatting / safety
|
||||
TELEGRAM_CONFIG_PATH = Path.home() / ".codex" / "telegram.toml"
|
||||
|
||||
|
||||
def _now_unix() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def _load_toml(path: Path) -> Dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
import tomllib # type: ignore[attr-defined]
|
||||
except ModuleNotFoundError:
|
||||
try:
|
||||
import tomli as tomllib # type: ignore[import-not-found]
|
||||
except ModuleNotFoundError as e:
|
||||
raise RuntimeError(
|
||||
f"TOML config found at {path} but tomllib/tomli is unavailable. "
|
||||
"Use Python 3.11+ or install tomli."
|
||||
) from e
|
||||
return tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_telegram_config(path: Optional[str] = None) -> Dict[str, Any]:
|
||||
cfg_path = Path(path) if path else TELEGRAM_CONFIG_PATH
|
||||
return _load_toml(cfg_path)
|
||||
|
||||
|
||||
def config_get(config: Dict[str, Any], key: str) -> Any:
|
||||
if key in config:
|
||||
return config[key]
|
||||
nested = config.get("telegram")
|
||||
if isinstance(nested, dict) and key in nested:
|
||||
return nested[key]
|
||||
return None
|
||||
|
||||
|
||||
def chunk_text(text: str, limit: int = DEFAULT_CHUNK_LEN) -> List[str]:
|
||||
"""
|
||||
Telegram hard limit is 4096 chars. Chunk at newlines when possible.
|
||||
@@ -237,3 +269,25 @@ def parse_allowed_chat_ids(env_value: str) -> Optional[set[int]]:
|
||||
continue
|
||||
out.add(int(part))
|
||||
return out
|
||||
|
||||
|
||||
def parse_chat_id_list(value: Any) -> Optional[set[int]]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return parse_allowed_chat_ids(value)
|
||||
if isinstance(value, int):
|
||||
return {value}
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
out: set[int] = set()
|
||||
for item in value:
|
||||
if item is None:
|
||||
continue
|
||||
if isinstance(item, str):
|
||||
if not item.strip():
|
||||
continue
|
||||
out.add(int(item))
|
||||
else:
|
||||
out.add(int(item))
|
||||
return out or None
|
||||
return None
|
||||
|
||||
@@ -9,7 +9,14 @@ import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from bridge_common import TelegramClient, RouteStore, parse_allowed_chat_ids
|
||||
from bridge_common import (
|
||||
TelegramClient,
|
||||
RouteStore,
|
||||
config_get,
|
||||
load_telegram_config,
|
||||
parse_allowed_chat_ids,
|
||||
parse_chat_id_list,
|
||||
)
|
||||
|
||||
# -------------------- Codex runner --------------------
|
||||
|
||||
@@ -138,17 +145,32 @@ class CodexExecRunner:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
db_path = os.environ.get("BRIDGE_DB", "./bridge_routes.sqlite3")
|
||||
config = load_telegram_config()
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN") or config_get(config, "bot_token") or ""
|
||||
db_path = os.environ.get("BRIDGE_DB") or config_get(config, "bridge_db") or "./bridge_routes.sqlite3"
|
||||
allowed = parse_allowed_chat_ids(os.environ.get("ALLOWED_CHAT_IDS", ""))
|
||||
startup_ids = parse_allowed_chat_ids(os.environ.get("STARTUP_CHAT_IDS", "")) or allowed
|
||||
startup_msg = os.environ.get("STARTUP_MESSAGE", "✅ exec_bridge started (codex exec).")
|
||||
if allowed is None:
|
||||
allowed = parse_chat_id_list(config_get(config, "allowed_chat_ids"))
|
||||
startup_ids = parse_allowed_chat_ids(os.environ.get("STARTUP_CHAT_IDS", ""))
|
||||
if startup_ids is None:
|
||||
startup_ids = parse_chat_id_list(config_get(config, "startup_chat_ids"))
|
||||
if startup_ids is None:
|
||||
startup_ids = allowed
|
||||
startup_msg = os.environ.get("STARTUP_MESSAGE") or config_get(
|
||||
config, "startup_message"
|
||||
) or "✅ exec_bridge started (codex exec)."
|
||||
startup_pwd = os.getcwd()
|
||||
startup_msg = f"{startup_msg}\nPWD: {startup_pwd}"
|
||||
|
||||
codex_cmd = os.environ.get("CODEX_CMD", "codex")
|
||||
workspace = os.environ.get("CODEX_WORKSPACE") # optional
|
||||
extra_args = shlex.split(os.environ.get("CODEX_EXEC_ARGS", "")) # e.g. "--full-auto --search"
|
||||
codex_cmd = os.environ.get("CODEX_CMD") or config_get(config, "codex_cmd") or "codex"
|
||||
workspace = os.environ.get("CODEX_WORKSPACE") or config_get(config, "codex_workspace")
|
||||
raw_exec_args = os.environ.get("CODEX_EXEC_ARGS")
|
||||
if raw_exec_args is None:
|
||||
raw_exec_args = config_get(config, "codex_exec_args") or ""
|
||||
if isinstance(raw_exec_args, list):
|
||||
extra_args = [str(v) for v in raw_exec_args]
|
||||
else:
|
||||
extra_args = shlex.split(str(raw_exec_args)) # e.g. "--full-auto --search"
|
||||
|
||||
def _has_notify_override(args: list[str]) -> bool:
|
||||
for i, arg in enumerate(args):
|
||||
|
||||
@@ -9,7 +9,14 @@ import time
|
||||
from queue import Queue, Empty
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from bridge_common import TelegramClient, RouteStore, parse_allowed_chat_ids
|
||||
from bridge_common import (
|
||||
TelegramClient,
|
||||
RouteStore,
|
||||
config_get,
|
||||
load_telegram_config,
|
||||
parse_allowed_chat_ids,
|
||||
parse_chat_id_list,
|
||||
)
|
||||
|
||||
MCP_PROTOCOL_VERSION = "2025-06-18"
|
||||
|
||||
@@ -244,18 +251,35 @@ class MCPStdioClient:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
db_path = os.environ.get("BRIDGE_DB", "./bridge_routes.sqlite3")
|
||||
config = load_telegram_config()
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN") or config_get(config, "bot_token") or ""
|
||||
db_path = os.environ.get("BRIDGE_DB") or config_get(config, "bridge_db") or "./bridge_routes.sqlite3"
|
||||
allowed = parse_allowed_chat_ids(os.environ.get("ALLOWED_CHAT_IDS", ""))
|
||||
if allowed is None:
|
||||
allowed = parse_chat_id_list(config_get(config, "allowed_chat_ids"))
|
||||
|
||||
# How to start Codex MCP server:
|
||||
# default: "codex mcp-server" (can also be "npx -y codex mcp-server")
|
||||
mcp_cmd = shlex.split(os.environ.get("CODEX_MCP_CMD", "codex mcp-server"))
|
||||
raw_mcp_cmd = os.environ.get("CODEX_MCP_CMD")
|
||||
if raw_mcp_cmd is None:
|
||||
raw_mcp_cmd = config_get(config, "codex_mcp_cmd") or "codex mcp-server"
|
||||
if isinstance(raw_mcp_cmd, list):
|
||||
mcp_cmd = [str(v) for v in raw_mcp_cmd]
|
||||
else:
|
||||
mcp_cmd = shlex.split(str(raw_mcp_cmd))
|
||||
|
||||
# Optional defaults for tool args (you can override as you like)
|
||||
default_cwd = os.environ.get("CODEX_WORKSPACE") # used as tool 'cwd'
|
||||
default_sandbox = os.environ.get("CODEX_SANDBOX", "workspace-write")
|
||||
default_approval = os.environ.get("CODEX_APPROVAL_POLICY", "never")
|
||||
default_cwd = os.environ.get("CODEX_WORKSPACE") or config_get(config, "codex_workspace")
|
||||
default_sandbox = (
|
||||
os.environ.get("CODEX_SANDBOX")
|
||||
or config_get(config, "codex_sandbox")
|
||||
or "workspace-write"
|
||||
)
|
||||
default_approval = (
|
||||
os.environ.get("CODEX_APPROVAL_POLICY")
|
||||
or config_get(config, "codex_approval_policy")
|
||||
or "never"
|
||||
)
|
||||
|
||||
bot = TelegramClient(token)
|
||||
store = RouteStore(db_path)
|
||||
|
||||
@@ -12,7 +12,18 @@ All options store a mapping from `(chat_id, bot_message_id)` to a route so repli
|
||||
|
||||
1. Ensure `uv` is installed.
|
||||
2. Use the scripts in this folder as-is (no extra dependencies).
|
||||
3. Set `TELEGRAM_BOT_TOKEN` and (optionally) `ALLOWED_CHAT_IDS`.
|
||||
3. Set `TELEGRAM_BOT_TOKEN` and (optionally) `ALLOWED_CHAT_IDS`, or put them in `~/.codex/telegram.toml`.
|
||||
|
||||
Example `~/.codex/telegram.toml`:
|
||||
|
||||
```toml
|
||||
bot_token = "123:abc"
|
||||
allowed_chat_ids = [123456789]
|
||||
startup_chat_ids = [123456789]
|
||||
startup_message = "✅ exec_bridge started (codex exec)."
|
||||
```
|
||||
|
||||
Environment variables always override the TOML file.
|
||||
|
||||
## Option 1: exec/resume
|
||||
|
||||
|
||||
@@ -5,19 +5,30 @@ import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from bridge_common import TelegramClient, RouteStore
|
||||
from bridge_common import TelegramClient, RouteStore, config_get, load_telegram_config
|
||||
|
||||
|
||||
def main() -> None:
|
||||
config = load_telegram_config()
|
||||
default_chat_id = config_get(config, "chat_id")
|
||||
if isinstance(default_chat_id, str):
|
||||
default_chat_id = int(default_chat_id) if default_chat_id.strip() else None
|
||||
elif not isinstance(default_chat_id, int):
|
||||
default_chat_id = None
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--chat-id", type=int, required=True)
|
||||
ap.add_argument("--chat-id", type=int, default=default_chat_id, required=default_chat_id is None)
|
||||
ap.add_argument("--tmux-target", type=str, required=True, help='tmux target, e.g. "codex1:0.0" or "codex1"')
|
||||
ap.add_argument("--db", type=str, default=os.environ.get("BRIDGE_DB", "./bridge_routes.sqlite3"))
|
||||
ap.add_argument(
|
||||
"--db",
|
||||
type=str,
|
||||
default=os.environ.get("BRIDGE_DB") or config_get(config, "bridge_db") or "./bridge_routes.sqlite3",
|
||||
)
|
||||
ap.add_argument("--reply-to", type=int, default=None, help="Optional Telegram message_id to reply to")
|
||||
ap.add_argument("--text", type=str, default=None, help="Message text. If omitted, read stdin.")
|
||||
args = ap.parse_args()
|
||||
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN") or config_get(config, "bot_token") or ""
|
||||
bot = TelegramClient(token)
|
||||
store = RouteStore(args.db)
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@ import subprocess
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from bridge_common import TelegramClient, RouteStore, parse_allowed_chat_ids
|
||||
from bridge_common import (
|
||||
TelegramClient,
|
||||
RouteStore,
|
||||
config_get,
|
||||
load_telegram_config,
|
||||
parse_allowed_chat_ids,
|
||||
parse_chat_id_list,
|
||||
)
|
||||
|
||||
|
||||
def tmux_send_text(target: str, text: str, press_enter: bool = True) -> None:
|
||||
@@ -21,9 +28,12 @@ def tmux_send_text(target: str, text: str, press_enter: bool = True) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
db_path = os.environ.get("BRIDGE_DB", "./bridge_routes.sqlite3")
|
||||
config = load_telegram_config()
|
||||
token = os.environ.get("TELEGRAM_BOT_TOKEN") or config_get(config, "bot_token") or ""
|
||||
db_path = os.environ.get("BRIDGE_DB") or config_get(config, "bridge_db") or "./bridge_routes.sqlite3"
|
||||
allowed = parse_allowed_chat_ids(os.environ.get("ALLOWED_CHAT_IDS", ""))
|
||||
if allowed is None:
|
||||
allowed = parse_chat_id_list(config_get(config, "allowed_chat_ids"))
|
||||
|
||||
bot = TelegramClient(token)
|
||||
store = RouteStore(db_path)
|
||||
|
||||
Reference in New Issue
Block a user