feat: add codex profile config

This commit is contained in:
banteg
2025-12-29 16:31:29 +04:00
parent caa9f4f99d
commit ed27ecd3c9
5 changed files with 67 additions and 67 deletions
+4 -3
View File
@@ -70,15 +70,16 @@ def render_markdown(md: str) -> tuple[str, list[dict[str, Any]]]:
### `config.py` — Configuration Loading ### `config.py` — Configuration Loading
```python ```python
def load_telegram_config(path=None) -> dict: def load_telegram_config(path=None, *, base_dir=None) -> tuple[dict, Path]:
# Loads ./codex/takopi.toml or ~/.codex/takopi.toml (or custom path) # Loads <base_dir>/codex/takopi.toml (if set), then ./codex/takopi.toml, then ~/.codex/takopi.toml
``` ```
### `constants.py` — Shared Constants ### `constants.py` — Shared Constants
```python ```python
TELEGRAM_HARD_LIMIT = 4096 # Max message length TELEGRAM_HARD_LIMIT = 4096 # Max message length
DEFAULT_CONFIG_PATHS = (./codex/takopi.toml, ~/.codex/takopi.toml) LOCAL_CONFIG_NAME = codex/takopi.toml
HOME_CONFIG_PATH = ~/.codex/takopi.toml
``` ```
### `logging.py` — Secure Logging Setup ### `logging.py` — Secure Logging Setup
+18 -1
View File
@@ -46,6 +46,23 @@ chat_id = 123456789
The bridge only accepts messages where the chat ID equals the sender ID and both match `chat_id` (i.e., private chat with that user). The bridge only accepts messages where the chat ID equals the sender ID and both match `chat_id` (i.e., private chat with that user).
When you pass `--cd`, Takopi looks for `codex/takopi.toml` under that directory first.
### Codex Profile (Optional)
Create a Codex profile in `~/.codex/config.toml`:
```toml
[profiles.takopi]
model = "gpt-4.1"
```
Then run Takopi with:
```bash
uv run takopi --profile takopi
```
### Running ### Running
```bash ```bash
@@ -59,7 +76,7 @@ uv run takopi
| `--final-notify` / `--no-final-notify` | `--final-notify` | Send final response as new message (vs. edit) | | `--final-notify` / `--no-final-notify` | `--final-notify` | Send final response as new message (vs. edit) |
| `--debug` / `--no-debug` | `--no-debug` | Enable verbose logging | | `--debug` / `--no-debug` | `--no-debug` | Enable verbose logging |
| `--cd PATH` | cwd | Working directory for Codex | | `--cd PATH` | cwd | Working directory for Codex |
| `--model NAME` | (codex default) | Model to use | | `--profile NAME` | (codex default) | Codex profile name |
## Usage ## Usage
+23 -19
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import tomllib import tomllib
from pathlib import Path from pathlib import Path
from .constants import DEFAULT_CONFIG_PATHS from .constants import HOME_CONFIG_PATH, LOCAL_CONFIG_NAME
class ConfigError(RuntimeError): class ConfigError(RuntimeError):
@@ -25,20 +25,21 @@ def _display_path(path: Path) -> str:
def _missing_config_message(primary: Path, alternate: Path | None = None) -> str: def _missing_config_message(primary: Path, alternate: Path | None = None) -> str:
if alternate is None: if alternate is None:
header = f"Missing config file `{_display_path(primary)}`." return f"Missing config file `{_display_path(primary)}`."
else: return "Missing takopi config. See readme.md for setup."
header = (
f"Missing config file `{_display_path(primary)}` "
f"(or `{_display_path(alternate)}`)." def _config_candidates(base_dir: Path | None) -> list[Path]:
) candidates: list[Path] = []
return "\n".join( if base_dir is not None:
[ candidates.append(base_dir / LOCAL_CONFIG_NAME)
header,
"Create it with:", cwd = Path.cwd()
' bot_token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"', if base_dir is None or base_dir != cwd:
" chat_id = 123456789", candidates.append(cwd / LOCAL_CONFIG_NAME)
]
) candidates.append(HOME_CONFIG_PATH)
return candidates
def _read_config(cfg_path: Path) -> dict: def _read_config(cfg_path: Path) -> dict:
@@ -54,14 +55,17 @@ def _read_config(cfg_path: Path) -> dict:
raise ConfigError(f"Malformed TOML in {cfg_path}: {e}") from None raise ConfigError(f"Malformed TOML in {cfg_path}: {e}") from None
def load_telegram_config(path: str | Path | None = None) -> tuple[dict, Path]: def load_telegram_config(
path: str | Path | None = None, *, base_dir: str | Path | None = None
) -> tuple[dict, Path]:
if path: if path:
cfg_path = Path(path).expanduser() cfg_path = Path(path).expanduser()
return _read_config(cfg_path), cfg_path return _read_config(cfg_path), cfg_path
local_path, home_path = DEFAULT_CONFIG_PATHS base = Path(base_dir).expanduser() if base_dir is not None else None
for candidate in (local_path, home_path): candidates = _config_candidates(base)
for candidate in candidates:
if candidate.is_file(): if candidate.is_file():
return _read_config(candidate), candidate return _read_config(candidate), candidate
raise ConfigError(_missing_config_message(home_path, local_path)) raise ConfigError(_missing_config_message(HOME_CONFIG_PATH, candidates[0]))
+2 -4
View File
@@ -3,7 +3,5 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
TELEGRAM_HARD_LIMIT = 4096 TELEGRAM_HARD_LIMIT = 4096
DEFAULT_CONFIG_PATHS = ( LOCAL_CONFIG_NAME = Path("codex") / "takopi.toml"
Path.cwd() / "codex" / "takopi.toml", HOME_CONFIG_PATH = Path.home() / ".codex" / "takopi.toml"
Path.home() / ".codex" / "takopi.toml",
)
+20 -40
View File
@@ -6,7 +6,6 @@ import json
import logging import logging
import os import os
import re import re
import shlex
import shutil import shutil
import time import time
from collections import deque from collections import deque
@@ -224,10 +223,11 @@ class CodexExecRunner:
"[codex] start run session_id=%r workspace=%r", session_id, self.workspace "[codex] start run session_id=%r workspace=%r", session_id, self.workspace
) )
logger.debug("[codex] prompt: %s", prompt) logger.debug("[codex] prompt: %s", prompt)
args = [self.codex_cmd, "exec", "--json"] args = [self.codex_cmd]
args.extend(self.extra_args) args.extend(self.extra_args)
if self.workspace: if self.workspace:
args.extend(["--cd", self.workspace]) args.extend(["--cd", self.workspace])
args.extend(["exec", "--json"])
# Always pipe prompt via stdin ("-") to avoid quoting issues. # Always pipe prompt via stdin ("-") to avoid quoting issues.
if session_id: if session_id:
@@ -356,9 +356,18 @@ def _parse_bridge_config(
*, *,
final_notify: bool, final_notify: bool,
cd: str | None, cd: str | None,
model: str | None, profile: str | None,
) -> BridgeConfig: ) -> BridgeConfig:
config, config_path = load_telegram_config() startup_pwd = os.getcwd()
workspace = None
if cd is not None:
expanded_cd = os.path.expanduser(cd)
if not os.path.isdir(expanded_cd):
raise ConfigError(f"--cd must be an existing directory: {expanded_cd}")
workspace = expanded_cd
startup_pwd = expanded_cd
config, config_path = load_telegram_config(base_dir=workspace)
try: try:
token = config["bot_token"] token = config["bot_token"]
except KeyError: except KeyError:
@@ -381,39 +390,10 @@ def _parse_bridge_config(
" brew install codex" " brew install codex"
) )
startup_pwd = os.getcwd()
workspace = None
if cd is not None:
expanded_cd = os.path.expanduser(cd)
if not os.path.isdir(expanded_cd):
raise ConfigError(f"--cd must be an existing directory: {expanded_cd}")
workspace = expanded_cd
startup_pwd = expanded_cd
startup_msg = f"codex exec bridge has started\npwd: {startup_pwd}" startup_msg = f"codex exec bridge has started\npwd: {startup_pwd}"
raw_exec_args = config.get("codex_exec_args", "") extra_args = ["-c", "notify=[]"]
if isinstance(raw_exec_args, list): if profile:
extra_args = [str(v) for v in raw_exec_args] extra_args.extend(["--profile", profile])
else:
extra_args = shlex.split(str(raw_exec_args)) # e.g. "--full-auto --search"
if model:
extra_args.extend(["--model", model])
def _has_notify_override(args: list[str]) -> bool:
for i, arg in enumerate(args):
if arg in ("-c", "--config"):
key = args[i + 1].split("=", 1)[0].strip()
if key == "notify" or key.endswith(".notify"):
return True
elif arg.startswith(("--config=", "-c=")):
key = arg.split("=", 1)[1].split("=", 1)[0].strip()
if key == "notify" or key.endswith(".notify"):
return True
return False
if not _has_notify_override(extra_args):
extra_args.extend(["-c", "notify=[]"])
bot = TelegramClient(token) bot = TelegramClient(token)
runner = CodexExecRunner(codex_cmd=codex_cmd, workspace=workspace, extra_args=extra_args) runner = CodexExecRunner(codex_cmd=codex_cmd, workspace=workspace, extra_args=extra_args)
@@ -716,10 +696,10 @@ def run(
"--cd", "--cd",
help="Pass through to `codex --cd`.", help="Pass through to `codex --cd`.",
), ),
model: str | None = typer.Option( profile: str | None = typer.Option(
None, None,
"--model", "--profile",
help="Codex model to pass to `codex exec`.", help="Codex profile name to pass to `codex --profile`.",
), ),
) -> None: ) -> None:
setup_logging(debug=debug) setup_logging(debug=debug)
@@ -727,7 +707,7 @@ def run(
cfg = _parse_bridge_config( cfg = _parse_bridge_config(
final_notify=final_notify, final_notify=final_notify,
cd=cd, cd=cd,
model=model, profile=profile,
) )
except ConfigError as e: except ConfigError as e:
typer.echo(str(e), err=True) typer.echo(str(e), err=True)