feat: streamline onboarding and add --version
This commit is contained in:
+1
-18
@@ -55,21 +55,12 @@ Transforms Codex JSONL events into human-readable text:
|
|||||||
| `render_event_cli()` | Simplified wrapper for console logging |
|
| `render_event_cli()` | Simplified wrapper for console logging |
|
||||||
| `ExecProgressRenderer` | Stateful renderer tracking recent actions for progress display |
|
| `ExecProgressRenderer` | Stateful renderer tracking recent actions for progress display |
|
||||||
| `format_elapsed()` | Formats seconds as `Xh Ym`, `Xm Ys`, or `Xs` |
|
| `format_elapsed()` | Formats seconds as `Xh Ym`, `Xm Ys`, or `Xs` |
|
||||||
|
| `render_markdown()` | Markdown → Telegram text + entities (markdown-it-py + sulguk) |
|
||||||
|
|
||||||
**Supported event types:**
|
**Supported event types:**
|
||||||
- `thread.started`, `turn.started/completed/failed`
|
- `thread.started`, `turn.started/completed/failed`
|
||||||
- `item.started/completed` for: `agent_message`, `reasoning`, `command_execution`, `mcp_tool_call`, `web_search`, `file_change`, `error`
|
- `item.started/completed` for: `agent_message`, `reasoning`, `command_execution`, `mcp_tool_call`, `web_search`, `file_change`, `error`
|
||||||
|
|
||||||
### `rendering.py` — Markdown to Telegram
|
|
||||||
|
|
||||||
Converts Markdown to Telegram-compatible text with entities:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def render_markdown(md: str) -> tuple[str, list[dict[str, Any]]]:
|
|
||||||
# Uses markdown-it-py + sulguk for entity extraction
|
|
||||||
# Fixes: replaces bullets, removes invalid language fields
|
|
||||||
```
|
|
||||||
|
|
||||||
### `config.py` — Configuration Loading
|
### `config.py` — Configuration Loading
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -77,14 +68,6 @@ def load_telegram_config(path=None) -> tuple[dict, Path]:
|
|||||||
# Loads ./.codex/takopi.toml, then ~/.codex/takopi.toml
|
# Loads ./.codex/takopi.toml, then ~/.codex/takopi.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
### `constants.py` — Shared Constants
|
|
||||||
|
|
||||||
```python
|
|
||||||
TELEGRAM_HARD_LIMIT = 4096 # Max message length
|
|
||||||
LOCAL_CONFIG_NAME = .codex/takopi.toml
|
|
||||||
HOME_CONFIG_PATH = ~/.codex/takopi.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### `logging.py` — Secure Logging Setup
|
### `logging.py` — Secure Logging Setup
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -74,6 +74,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 |
|
||||||
| `--profile NAME` | (codex default) | Codex profile name |
|
| `--profile NAME` | (codex default) | Codex profile name |
|
||||||
|
| `--version` | | Show the version and exit |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
"""Takopi — Telegram Codex bridge package."""
|
"""Takopi — Telegram Codex bridge package."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|||||||
+5
-33
@@ -3,42 +3,14 @@ from __future__ import annotations
|
|||||||
import tomllib
|
import tomllib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .constants import HOME_CONFIG_PATH, LOCAL_CONFIG_NAME
|
LOCAL_CONFIG_NAME = Path(".codex") / "takopi.toml"
|
||||||
|
HOME_CONFIG_PATH = Path.home() / ".codex" / "takopi.toml"
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(RuntimeError):
|
class ConfigError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
_EXAMPLE_CONFIG = (
|
|
||||||
'bot_token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"\nchat_id = 123456789\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _display_path(path: Path) -> str:
|
|
||||||
cwd = Path.cwd()
|
|
||||||
if path.is_relative_to(cwd):
|
|
||||||
return f"./{path.relative_to(cwd).as_posix()}"
|
|
||||||
home = Path.home()
|
|
||||||
if path.is_relative_to(home):
|
|
||||||
return f"~/{path.relative_to(home).as_posix()}"
|
|
||||||
return str(path)
|
|
||||||
|
|
||||||
|
|
||||||
def _missing_config_message(primary: Path, alternate: Path | None = None) -> str:
|
|
||||||
example = "Example config:\n```\n" + _EXAMPLE_CONFIG + "```\n"
|
|
||||||
if alternate is None:
|
|
||||||
return f"Missing config file `{_display_path(primary)}`.\n{example}"
|
|
||||||
return (
|
|
||||||
"Missing takopi config.\n"
|
|
||||||
"Create one of these files:\n"
|
|
||||||
f" {_display_path(alternate)}\n"
|
|
||||||
f" {_display_path(primary)}\n"
|
|
||||||
"\n"
|
|
||||||
f"{example}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _config_candidates() -> list[Path]:
|
def _config_candidates() -> list[Path]:
|
||||||
candidates = [Path.cwd() / LOCAL_CONFIG_NAME, HOME_CONFIG_PATH]
|
candidates = [Path.cwd() / LOCAL_CONFIG_NAME, HOME_CONFIG_PATH]
|
||||||
if candidates[0] == candidates[1]:
|
if candidates[0] == candidates[1]:
|
||||||
@@ -50,7 +22,7 @@ def _read_config(cfg_path: Path) -> dict:
|
|||||||
try:
|
try:
|
||||||
raw = cfg_path.read_text(encoding="utf-8")
|
raw = cfg_path.read_text(encoding="utf-8")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise ConfigError(_missing_config_message(cfg_path)) from None
|
raise ConfigError(f"Missing config file {cfg_path}.") from None
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise ConfigError(f"Failed to read config file {cfg_path}: {e}") from e
|
raise ConfigError(f"Failed to read config file {cfg_path}: {e}") from e
|
||||||
try:
|
try:
|
||||||
@@ -70,5 +42,5 @@ def load_telegram_config(path: str | Path | None = None) -> tuple[dict, Path]:
|
|||||||
return _read_config(candidate), candidate
|
return _read_config(candidate), candidate
|
||||||
|
|
||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
raise ConfigError(_missing_config_message(candidates[0]))
|
raise ConfigError("Missing takopi config.")
|
||||||
raise ConfigError(_missing_config_message(HOME_CONFIG_PATH, candidates[0]))
|
raise ConfigError("Missing takopi config.")
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TELEGRAM_HARD_LIMIT = 4096
|
|
||||||
LOCAL_CONFIG_NAME = Path(".codex") / "takopi.toml"
|
|
||||||
HOME_CONFIG_PATH = Path.home() / ".codex" / "takopi.toml"
|
|
||||||
@@ -17,10 +17,10 @@ from weakref import WeakValueDictionary
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from .config import ConfigError, load_telegram_config
|
from .config import ConfigError, load_telegram_config
|
||||||
from .exec_render import ExecProgressRenderer, render_event_cli
|
from .exec_render import ExecProgressRenderer, render_event_cli, render_markdown
|
||||||
from .logging import setup_logging
|
from .logging import setup_logging
|
||||||
from .rendering import render_markdown
|
|
||||||
from .onboarding import check_setup, render_setup_guide
|
from .onboarding import check_setup, render_setup_guide
|
||||||
from .telegram_client import TelegramClient
|
from .telegram_client import TelegramClient
|
||||||
|
|
||||||
@@ -33,6 +33,16 @@ RESUME_LINE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_version_and_exit() -> None:
|
||||||
|
typer.echo(__version__)
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
|
def _version_callback(value: bool) -> None:
|
||||||
|
if value:
|
||||||
|
_print_version_and_exit()
|
||||||
|
|
||||||
|
|
||||||
def extract_session_id(text: str | None) -> str | None:
|
def extract_session_id(text: str | None) -> str | None:
|
||||||
if not text:
|
if not text:
|
||||||
return None
|
return None
|
||||||
@@ -663,6 +673,13 @@ async def _run_main_loop(cfg: BridgeConfig) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
|
version: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--version",
|
||||||
|
help="Show the version and exit.",
|
||||||
|
callback=_version_callback,
|
||||||
|
is_eager=True,
|
||||||
|
),
|
||||||
final_notify: bool = typer.Option(
|
final_notify: bool = typer.Option(
|
||||||
True,
|
True,
|
||||||
"--final-notify/--no-final-notify",
|
"--final-notify/--no-final-notify",
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ from collections import deque
|
|||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from markdown_it import MarkdownIt
|
||||||
|
from sulguk import transform_html
|
||||||
|
|
||||||
STATUS_RUNNING = "▸"
|
STATUS_RUNNING = "▸"
|
||||||
STATUS_DONE = "✓"
|
STATUS_DONE = "✓"
|
||||||
STATUS_FAIL = "✗"
|
STATUS_FAIL = "✗"
|
||||||
@@ -16,6 +19,24 @@ MAX_PROGRESS_CMD_LEN = 300
|
|||||||
MAX_QUERY_LEN = 60
|
MAX_QUERY_LEN = 60
|
||||||
MAX_PATH_LEN = 40
|
MAX_PATH_LEN = 40
|
||||||
|
|
||||||
|
_md = MarkdownIt("commonmark", {"html": False})
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(md: str) -> tuple[str, list[dict[str, Any]]]:
|
||||||
|
html = _md.render(md or "")
|
||||||
|
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: list[dict[str, Any]] = []
|
||||||
|
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)
|
||||||
|
return text, entities
|
||||||
|
|
||||||
|
|
||||||
def format_elapsed(elapsed_s: float) -> str:
|
def format_elapsed(elapsed_s: float) -> str:
|
||||||
total = max(0, int(elapsed_s))
|
total = max(0, int(elapsed_s))
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from pathlib import Path
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
from .config import ConfigError, load_telegram_config
|
from .config import ConfigError, HOME_CONFIG_PATH, load_telegram_config
|
||||||
from .constants import HOME_CONFIG_PATH
|
|
||||||
|
|
||||||
_OCTOPUS = "\N{OCTOPUS}"
|
_OCTOPUS = "\N{OCTOPUS}"
|
||||||
|
|
||||||
@@ -100,7 +99,7 @@ def render_setup_guide(result: SetupResult) -> None:
|
|||||||
"[bold]Getting your Telegram credentials:[/]",
|
"[bold]Getting your Telegram credentials:[/]",
|
||||||
"",
|
"",
|
||||||
" [cyan]bot_token[/] create a bot with [link=https://t.me/BotFather]@BotFather[/]",
|
" [cyan]bot_token[/] create a bot with [link=https://t.me/BotFather]@BotFather[/]",
|
||||||
" [cyan]chat_id[/] get from [link=https://t.me/myidbot]@myidbot[/]",
|
" [cyan]chat_id[/] message [link=https://t.me/myidbot]@myidbot[/] to get your id",
|
||||||
)
|
)
|
||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from markdown_it import MarkdownIt
|
|
||||||
from sulguk import transform_html
|
|
||||||
|
|
||||||
_md = MarkdownIt("commonmark", {"html": False})
|
|
||||||
|
|
||||||
|
|
||||||
def render_markdown(md: str) -> tuple[str, list[dict[str, Any]]]:
|
|
||||||
html = _md.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: list[dict[str, Any]] = []
|
|
||||||
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)
|
|
||||||
return text, entities
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from takopi.exec_render import ExecProgressRenderer, render_event_cli
|
from takopi.exec_render import ExecProgressRenderer, render_event_cli, render_markdown
|
||||||
from takopi.rendering import render_markdown
|
|
||||||
|
|
||||||
|
|
||||||
def _loads(lines: str) -> list[dict]:
|
def _loads(lines: str) -> list[dict]:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from takopi.rendering import render_markdown
|
from takopi.exec_render import render_markdown
|
||||||
|
|
||||||
|
|
||||||
def test_render_markdown_basic_entities() -> None:
|
def test_render_markdown_basic_entities() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user