diff --git a/takopi/developing.md b/developing.md similarity index 100% rename from takopi/developing.md rename to developing.md diff --git a/notify_telegram/notify_telegram.py b/notify_telegram/notify_telegram.py deleted file mode 100644 index 5bf25ac..0000000 --- a/notify_telegram/notify_telegram.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -# /// script -# requires-python = ">=3.10" -# dependencies = ["requests", "markdown-it-py", "sulguk", "tomli; python_version < '3.11'"] -# /// -import json -import re -import sys -from pathlib import Path - -import requests -from markdown_it import MarkdownIt -from sulguk import transform_html - -CONFIG_PATH = Path.home() / ".codex" / "telegram.toml" -ERR_PATH = Path.home() / ".codex" / "telegram_last_error.txt" - - -def _load_toml(path: Path) -> dict: - if not path.exists(): - return {} - try: - import tomllib # type: ignore[attr-defined] - except ModuleNotFoundError: - import tomli as tomllib # type: ignore[import-not-found] - return tomllib.loads(path.read_text(encoding="utf-8")) - - -def _config_get(config: dict, key: str): - 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 main() -> None: - config = _load_toml(CONFIG_PATH) - bot_token = _config_get(config, "bot_token") - chat_id = _config_get(config, "chat_id") - if not bot_token or chat_id is None: - raise KeyError("telegram.toml must include bot_token and chat_id") - bot_token = str(bot_token) - chat_id = str(chat_id) - - event = json.loads(sys.argv[1]) - - md = event["last-assistant-message"].rstrip() - thread_id = event.get("thread-id") - if thread_id: - md += f"\n\nthread: `{thread_id}`" - - html = MarkdownIt("commonmark", {"html": False}).render(md) - rendered = transform_html(html) - - text = re.sub(r"(?m)^(\s*)•", r"\1-", rendered.text) - - # FIX: Telegram requires MessageEntity.language (if present) to be a String. - entities = [] - for e in rendered.entities: - d = dict(e) - if "language" in d and not isinstance(d["language"], str): - d.pop("language", None) - entities.append(d) - - r = requests.post( - f"https://api.telegram.org/bot{bot_token}/sendMessage", - json={ - "chat_id": chat_id, - "text": text, - "entities": entities, - "disable_web_page_preview": True, - }, - timeout=15, - ) - - try: - data = r.json() - except Exception: - data = {"ok": False, "description": r.text} - - if not (r.status_code == 200 and data.get("ok") is True): - ERR_PATH.write_text( - f"{r.status_code}\n{data.get('description','')}\n", - encoding="utf-8", - ) - - -if __name__ == "__main__": - try: - main() - except Exception: - pass diff --git a/notify_telegram/readme.md b/notify_telegram/readme.md deleted file mode 100644 index 58cd66b..0000000 --- a/notify_telegram/readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# Notify Telegram (Codex) - -Send Codex completion summaries to Telegram with safe Markdown rendering and stable list bullets. - -## Install - -1. Ensure `uv` is installed. -2. Copy the script to `~/.codex/notify_telegram.py`. -3. Create your [Telegram creds](https://t.me/botfather) file at `~/.codex/telegram.toml`. - -Example: - -```toml -bot_token = "123456:ABCDEF..." -chat_id = 462722 -``` - -## Configure - -Add a `notify` entry to `~/.codex/config.toml`: - -```toml -notify = ["uv", "run", "-q", "/home/user/.codex/notify_telegram.py"] -``` - -## Notes - -- The script reads `last-assistant-message` and treats it as Markdown. -- Markdown is rendered to HTML, converted to Telegram text/entities via `sulguk`, then posted with `requests`. -- List bullets are normalized from `•` to `-` to keep Telegram output consistent. - -## Files - -- `notify_telegram.py`: the notifier script diff --git a/takopi/pyproject.toml b/pyproject.toml similarity index 100% rename from takopi/pyproject.toml rename to pyproject.toml diff --git a/readme.md b/readme.md index af0b887..810b865 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,104 @@ -# Scripts +# Takopi -## Codex +> 🐙 A little helper from Happy Planet, here to make your Codex sessions happier-pi! -- [Notify Telegram](notify_telegram/readme.md) — Send Codex completion summaries to Telegram with safe Markdown rendering. -- [Takopi](takopi/readme.md) — Route Telegram replies into Codex sessions (exec, MCP, or tmux). +A Telegram bot that bridges messages to [Codex](https://github.com/openai/codex) sessions using non-interactive `codex exec` and `codex exec resume`. + +## Features + +- **Stateless Resume**: No database required—sessions are resumed via `resume: ` lines embedded in messages +- **Progress Updates**: Real-time progress edits showing commands, tools, and elapsed time +- **Markdown Rendering**: Full Telegram-compatible markdown with entity support +- **Concurrency**: Handles multiple conversations with per-session serialization +- **Token Redaction**: Automatically redacts Telegram tokens from logs + +## Quick Start + +### Prerequisites + +- Python 3.12+ +- [uv](https://github.com/astral-sh/uv) package manager +- Codex CLI on PATH + +### Installation + +```bash +# Clone and enter the directory +cd takopi + +# Run directly with uv (installs deps automatically) +uv run takopi --help +``` + +### Configuration + +Create `~/.codex/telegram.toml`: + +```toml +bot_token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz" +chat_id = 123456789 +``` + +| Key | Description | +|-----|-------------| +| `bot_token` | Telegram Bot API token from [@BotFather](https://t.me/BotFather) | +| `chat_id` | Allowed chat ID (also used for startup notifications) | + +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). + +### Running + +```bash +uv run takopi +``` + +#### Options + +| Flag | Default | Description | +|------|---------|-------------| +| `--final-notify` / `--no-final-notify` | `--final-notify` | Send final response as new message (vs. edit) | +| `--debug` / `--no-debug` | `--no-debug` | Enable verbose logging | +| `--cd PATH` | cwd | Working directory for Codex | +| `--model NAME` | (codex default) | Model to use | + +## Usage + +### New Conversation + +Send any message to your bot. The bridge will: + +1. Send a silent progress message +2. Stream events from `codex exec` +3. Update progress every ~2 seconds +4. Send final response with session ID + +### Resume a Session + +Reply to a bot message (containing `resume: `), or include the resume line in your message: + +``` +resume: `019b66fc-64c2-7a71-81cd-081c504cfeb2` +``` + +## Behavior Notes + +- **Startup**: Pending updates are drained (ignored) on startup +- **Progress**: Updates are throttled to ~2s intervals, sent silently +- **Notifications**: Codex's built-in notify is disabled (bridge handles it) +- **Filtering**: Only accepts messages where chat ID equals sender ID and matches `chat_id` + +## Development + +See [`developing.md`](developing.md) for architecture details. + +```bash +# Run tests +uv run pytest + +# Run with debug logging +uv run takopi --debug 2>&1 | tee debug.log +``` + +## License + +MIT diff --git a/takopi/src/takopi/__init__.py b/src/takopi/__init__.py similarity index 100% rename from takopi/src/takopi/__init__.py rename to src/takopi/__init__.py diff --git a/takopi/src/takopi/config.py b/src/takopi/config.py similarity index 100% rename from takopi/src/takopi/config.py rename to src/takopi/config.py diff --git a/takopi/src/takopi/constants.py b/src/takopi/constants.py similarity index 100% rename from takopi/src/takopi/constants.py rename to src/takopi/constants.py diff --git a/takopi/src/takopi/exec_bridge.py b/src/takopi/exec_bridge.py similarity index 100% rename from takopi/src/takopi/exec_bridge.py rename to src/takopi/exec_bridge.py diff --git a/takopi/src/takopi/exec_render.py b/src/takopi/exec_render.py similarity index 100% rename from takopi/src/takopi/exec_render.py rename to src/takopi/exec_render.py diff --git a/takopi/src/takopi/logging.py b/src/takopi/logging.py similarity index 100% rename from takopi/src/takopi/logging.py rename to src/takopi/logging.py diff --git a/takopi/src/takopi/rendering.py b/src/takopi/rendering.py similarity index 100% rename from takopi/src/takopi/rendering.py rename to src/takopi/rendering.py diff --git a/takopi/src/takopi/telegram_client.py b/src/takopi/telegram_client.py similarity index 100% rename from takopi/src/takopi/telegram_client.py rename to src/takopi/telegram_client.py diff --git a/takopi/readme.md b/takopi/readme.md deleted file mode 100644 index 810b865..0000000 --- a/takopi/readme.md +++ /dev/null @@ -1,104 +0,0 @@ -# Takopi - -> 🐙 A little helper from Happy Planet, here to make your Codex sessions happier-pi! - -A Telegram bot that bridges messages to [Codex](https://github.com/openai/codex) sessions using non-interactive `codex exec` and `codex exec resume`. - -## Features - -- **Stateless Resume**: No database required—sessions are resumed via `resume: ` lines embedded in messages -- **Progress Updates**: Real-time progress edits showing commands, tools, and elapsed time -- **Markdown Rendering**: Full Telegram-compatible markdown with entity support -- **Concurrency**: Handles multiple conversations with per-session serialization -- **Token Redaction**: Automatically redacts Telegram tokens from logs - -## Quick Start - -### Prerequisites - -- Python 3.12+ -- [uv](https://github.com/astral-sh/uv) package manager -- Codex CLI on PATH - -### Installation - -```bash -# Clone and enter the directory -cd takopi - -# Run directly with uv (installs deps automatically) -uv run takopi --help -``` - -### Configuration - -Create `~/.codex/telegram.toml`: - -```toml -bot_token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz" -chat_id = 123456789 -``` - -| Key | Description | -|-----|-------------| -| `bot_token` | Telegram Bot API token from [@BotFather](https://t.me/BotFather) | -| `chat_id` | Allowed chat ID (also used for startup notifications) | - -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). - -### Running - -```bash -uv run takopi -``` - -#### Options - -| Flag | Default | Description | -|------|---------|-------------| -| `--final-notify` / `--no-final-notify` | `--final-notify` | Send final response as new message (vs. edit) | -| `--debug` / `--no-debug` | `--no-debug` | Enable verbose logging | -| `--cd PATH` | cwd | Working directory for Codex | -| `--model NAME` | (codex default) | Model to use | - -## Usage - -### New Conversation - -Send any message to your bot. The bridge will: - -1. Send a silent progress message -2. Stream events from `codex exec` -3. Update progress every ~2 seconds -4. Send final response with session ID - -### Resume a Session - -Reply to a bot message (containing `resume: `), or include the resume line in your message: - -``` -resume: `019b66fc-64c2-7a71-81cd-081c504cfeb2` -``` - -## Behavior Notes - -- **Startup**: Pending updates are drained (ignored) on startup -- **Progress**: Updates are throttled to ~2s intervals, sent silently -- **Notifications**: Codex's built-in notify is disabled (bridge handles it) -- **Filtering**: Only accepts messages where chat ID equals sender ID and matches `chat_id` - -## Development - -See [`developing.md`](developing.md) for architecture details. - -```bash -# Run tests -uv run pytest - -# Run with debug logging -uv run takopi --debug 2>&1 | tee debug.log -``` - -## License - -MIT diff --git a/takopi/tests/conftest.py b/tests/conftest.py similarity index 100% rename from takopi/tests/conftest.py rename to tests/conftest.py diff --git a/takopi/tests/fixtures/codex.jsonl b/tests/fixtures/codex.jsonl similarity index 100% rename from takopi/tests/fixtures/codex.jsonl rename to tests/fixtures/codex.jsonl diff --git a/takopi/tests/test_exec_bridge.py b/tests/test_exec_bridge.py similarity index 100% rename from takopi/tests/test_exec_bridge.py rename to tests/test_exec_bridge.py diff --git a/takopi/tests/test_exec_render.py b/tests/test_exec_render.py similarity index 100% rename from takopi/tests/test_exec_render.py rename to tests/test_exec_render.py diff --git a/takopi/tests/test_exec_runner.py b/tests/test_exec_runner.py similarity index 100% rename from takopi/tests/test_exec_runner.py rename to tests/test_exec_runner.py diff --git a/takopi/tests/test_rendering.py b/tests/test_rendering.py similarity index 100% rename from takopi/tests/test_rendering.py rename to tests/test_rendering.py diff --git a/takopi/tests/test_subprocess.py b/tests/test_subprocess.py similarity index 100% rename from takopi/tests/test_subprocess.py rename to tests/test_subprocess.py diff --git a/takopi/tests/test_telegram_client.py b/tests/test_telegram_client.py similarity index 100% rename from takopi/tests/test_telegram_client.py rename to tests/test_telegram_client.py