feat: add codex profile config
This commit is contained in:
+4
-3
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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]))
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user