tgbot
tgbot is a Dart CLI that connects a Telegram bot to AI CLIs (codex, cursor, opencode, gemini, claude). It long-polls Telegram, forwards authorized messages into the configured provider CLI, keeps a per-chat thread ID in memory, and sends streamed text plus local file/image artifacts back to Telegram.
Install
dart pub global activate tgbot
If Dart's global bin directory is not already on your PATH, add it first so tgbot is runnable from the shell.
Requirements
- Dart SDK
- One supported provider CLI installed and available on
PATH(codex,cursor-agent,opencode,gemini, orclaude), unless overridden withai_cli_cmd - A Telegram bot token from
@BotFather - Your Telegram numeric user ID
Provider CLI references:
Quick Start
Create a starter config:
tgbot init
You can also start from the checked-in example: [tgbot.yaml.example](tgbot.yaml.example).
Edit tgbot.yaml:
bots:
- name: my-bot
telegram_bot_token: "YOUR_TELEGRAM_BOT_TOKEN"
allowed_user_ids:
- 123456789
project_path: /absolute/path/to/project
Validate it:
tgbot validate
Start the bridge:
tgbot start
Then open your bot in Telegram and send it a message.
CLI
tgbot <command> [options]
| Command | Description |
|---|---|
start |
Start all bots declared in the config file |
init |
Generate a starter tgbot.yaml |
validate |
Parse and validate a config file without starting bots |
upgrade |
Reinstall the latest published tgbot with Dart |
Global flags:
| Flag | Description |
|---|---|
-h, --help |
Print usage help |
-v, --version |
Print the version |
Examples:
tgbot start
tgbot start -c custom.yaml
tgbot init
tgbot init -o other.yaml
tgbot validate
tgbot validate -c custom.yaml
tgbot upgrade
Telegram Setup
Bot token
- Open Telegram and find
@BotFather. - Run
/newbot. - Follow the prompts.
- Copy the token into
telegram_bot_token.
User ID
- Open Telegram and find
@userinfobot. - Send it any message.
- Copy the numeric ID into
allowed_user_ids.
Group ID (optional)
- Add your bot to a group/supergroup.
- Send any message in that chat.
- Use
getUpdatesand copymessage.chat.idintoallowed_chat_ids(usually a negative number like-1001234567890).
Messages are accepted when either the sender is in allowed_user_ids or the chat is in allowed_chat_ids.
Configuration
tgbot loads one YAML file, defaulting to tgbot.yaml.
Top-level keys:
bots(required): non-empty list of bot definitionsdefaults(optional): shared values inherited by each bot
Minimal config
bots:
- name: my-bot
telegram_bot_token: "YOUR_TELEGRAM_BOT_TOKEN"
allowed_user_ids:
- 123456789
# Optional:
# allowed_chat_ids:
# - -1001234567890
project_path: /absolute/path/to/project
# Optional forum topic creation by name:
# topics:
# - name: Backend
# project_path: /absolute/path/to/backend-project
Config with shared defaults
defaults:
project_path: /absolute/path/to/default/project
provider: codex
ai_cli_cmd: codex
ai_cli_args:
- --model
- gpt-5
poll_timeout_sec: 60
ai_cli_timeout_sec: 1000
bots:
- name: repo-a
telegram_bot_token: "TOKEN_A"
allowed_user_ids:
- 123456789
- name: repo-b
telegram_bot_token: "TOKEN_B"
allowed_user_ids:
- 123456789
project_path: /absolute/path/to/repo-b
additional_system_prompt: |
Keep answers brief and focus on production issues.
memory: true
memory_filename: TEAM_MEMORY.md
Bot fields
| Key | Required | Notes |
|---|---|---|
name |
Yes | Used in logs and status messages |
telegram_bot_token |
Yes | Telegram Bot API token |
allowed_user_ids |
No* | Telegram user IDs allowed to use the bot |
allowed_chat_ids |
No* | Telegram chat IDs allowed to use the bot (groups/supergroups/channels) |
project_path |
No* | Default working directory used when a message does not match a configured topic; may be inherited from defaults |
topics |
No | List of forum topics to create/manage by name, optional chat_id, project_path, and topic-level overrides |
provider |
No | Provider name: codex, cursor, opencode, gemini, or claude; default codex |
ai_cli_cmd |
No | Executable to run for the selected provider, defaults by provider (codex, cursor-agent, opencode, gemini, claude) |
ai_cli_args |
No | Extra args passed to the provider CLI; accepts a YAML list or whitespace-delimited string |
poll_timeout_sec |
No | Telegram long-poll timeout in seconds, default 60 |
ai_cli_timeout_sec |
No | Per-request provider timeout in seconds, default 1000 |
additional_system_prompt |
No | Extra system instructions prepended only for the first question in a session |
memory |
No | When true, injects memory-file management instructions only for the first question in a session; default false |
memory_filename |
No | Filename used by memory instructions when memory is enabled; default MEMORY.md |
final_response_only |
No | When true, suppresses streamed partial messages and sends only the final assistant response, default true |
telegram_commands |
No | Extra Telegram slash commands registered for this bot |
log_level |
No | Runtime log level: debug, info, warn, error; default info |
log_format |
No | Runtime log format: text or json; default text |
strict_config |
No | When true, unknown keys are rejected during config parsing; default false |
validate_project_path |
No | When true, project_path must exist and be readable at startup; default false |
Notes:
defaultsapplies only when a bot omits that key.project_pathis required unless the bot configures at least one topic intopics.topics[*].project_pathvalues are normalized to absolute paths at startup.topics[*].chat_idmay be omitted only when the bot has exactly one effectiveallowed_chat_idsvalue; otherwise it is required.- At least one of
allowed_user_idsorallowed_chat_idsis required. telegram_commandsmust be a non-empty list when provided.- Command names must match
^[a-z0-9_]{1,32}$. - Built-in
/start,/new,/stop, and/restartcommands are always present unless you override them intelegram_commands. project_pathis normalized to an absolute path at startup when provided.- Name-based topic creation is cached in
.tgbot-topic-registry.jsonso createdmessage_thread_idvalues are reused on restart. - In
strict_config: true, unknown keys inroot,defaults,bots[*], andtelegram_commands[*]fail fast. ai_cli_argsstring values support shell-like quoting (single quotes, double quotes, and escapes).
Topic creation by name
If you want the bot to create forum topics for you, configure them by name:
bots:
- name: my-bot
telegram_bot_token: "YOUR_TELEGRAM_BOT_TOKEN"
allowed_user_ids:
- 123456789
allowed_chat_ids:
- -1001234567890
topics:
- name: Backend
project_path: /absolute/path/to/backend-project
- name: Mobile
project_path: /absolute/path/to/mobile-project
Notes:
- The bot calls Telegram
createForumTopicfor entries not already present in the local registry. - Telegram returns a
message_thread_id, and the bot stores it in.tgbot-topic-registry.json. - If the bot has exactly one
allowed_chat_idsentry,topics[*].chat_idcan be omitted and that chat is used automatically. - Telegram Bot API does not provide a topic-by-name lookup endpoint, so deleting that registry file can cause duplicate topics to be created on the next startup.
- The bot must be an admin in the target supergroup and allowed to manage topics.
Supported topic-level overrides:
project_pathtelegram_commandsadditional_system_promptmemorymemory_filenamefinal_response_only
Example with topic-specific command and response behavior:
bots:
- name: my-bot
telegram_bot_token: "YOUR_TELEGRAM_BOT_TOKEN"
allowed_chat_ids:
- -1001234567890
topics:
- name: Backend
project_path: /absolute/path/to/backend-project
final_response_only: true
additional_system_prompt: |
Focus on backend services only.
telegram_commands:
- command: topic_fix
description: Fix this backend issue: {args}
Custom Telegram Commands
You can register Telegram slash commands that map to prompt templates:
bots:
- name: my-bot
telegram_bot_token: "YOUR_TELEGRAM_BOT_TOKEN"
allowed_user_ids:
- 123456789
project_path: /absolute/path/to/project
telegram_commands:
- command: review
description: Review the current branch and list bugs first.
- command: fix
description: Fix this issue: {args}
Behavior:
- Telegram shows these commands via
setMyCommands. - If
descriptioncontains{args}, text after the command replaces that placeholder. - Otherwise command arguments are appended to the description with a blank line.
Examples:
/reviewbecomesReview the current branch and list bugs first./fix login racebecomesFix this issue: login race
Runtime Behavior
- One polling loop runs per configured bot.
- Messages are processed only when sender user ID is in
allowed_user_idsor chat ID is inallowed_chat_ids. - Messages are processed serially per chat topic so responses stay ordered without blocking unrelated topics in the same forum chat.
/startreturns a short help message plus the registered command list./newclears the in-memory thread/session id for that Telegram chat topic./stopterminates the active provider CLI process for that Telegram chat topic, if one is running./restartreloads the same YAML config file used at startup and restarts all bots in this process./restartis allowed only if the sender ID is listed inallowed_user_idsfor every configured bot./restartuses rollback semantics: if reload/startup fails, existing bots keep running.- If a run is already in progress for a chat topic, new prompts wait in that topic's queue until the active run finishes or is stopped.
- Any other text message is forwarded to the configured provider CLI.
- The current thread/session id is stored per chat topic and reused until
/newor process restart. tgbot starthandlesSIGINT/SIGTERMand shuts down polling, active runs, and HTTP clients gracefully.
Provider invocation patterns:
codex ...args exec --skip-git-repo-check --json <prompt>
cursor-agent ...args --print --output-format stream-json <prompt>
opencode ...args run --format json <prompt>
gemini ...args --prompt <prompt> --output-format json
claude ...args --verbose --print --output-format stream-json <prompt>
Resume behavior:
codex ...args exec resume --skip-git-repo-check --json <thread_id> <prompt>
cursor-agent ...args --print --output-format stream-json --resume <thread_id> <prompt>
opencode ...args run --session <thread_id> --format json <prompt>
gemini ...args --resume <thread_id> --prompt <prompt> --output-format json
claude ...args --verbose --print --output-format stream-json --resume <thread_id> <prompt>
Notes:
cursor,gemini,opencode, andclaudesession ids are captured from provider output when available.- For
cursor,tgbotadds--trustautomatically unless you already pass one of--trust,--yolo, or-finai_cli_args.
Streaming and Replies
By default, tgbot reads provider output incrementally when streaming JSON is available and forwards assistant messages to Telegram as they arrive. It also:
- keeps Telegram's typing indicator active while the provider CLI is running
- avoids re-sending duplicate streamed messages
- chunks long Telegram messages at newline or word boundaries
- falls back to reconstructed assistant messages if only final JSON output is available
If final_response_only: true is set for a bot, streamed partial replies are not sent and only the final assistant response is delivered.
File and Image Delivery
tgbot can send local artifacts back through Telegram during the same reply flow.
Preferred mechanism:
TG_ARTIFACT: {"kind":"image","path":"artifacts/plot.png","caption":"Latest chart"}
Supported artifact detection:
TG_ARTIFACT: {...}marker lines- standalone JSON artifact objects in provider output
- local Markdown image/link syntax when no explicit artifact marker is present
Rules:
- image files are uploaded with
sendPhoto - non-image files are uploaded with
sendDocument - relative paths are resolved from
project_path - artifact paths must stay inside
project_path - missing files and path traversal are rejected
Error Handling
- Telegram API calls retry up to 3 times on HTTP
429 - Telegram
retry_aftervalues are respected - empty outgoing messages are skipped
- Provider CLI failures and timeouts are reported back to the Telegram chat
- bot tokens are never logged
Project Layout
bin/tgbot.dart: CLI entry point and subcommandslib/tgbot.dart: public package exportslib/src/app.dart: bridge runtime and message routinglib/src/config.dart: YAML parsing and validationlib/src/runner/base_runner.dart: shared runner orchestration (process lifecycle, streaming, cancellation)lib/src/runner/ai_cli_runner.dart: provider runner interface and result typeslib/src/runner/runner_factory.dart: factory that builds provider-specific runners from configlib/src/runner/codex_runner.dart: Codex providerlib/src/runner/cursor_runner.dart: Cursor providerlib/src/runner/claude_runner.dart: Claude providerlib/src/runner/gemini_runner.dart: Gemini providerlib/src/runner/opencode_runner.dart: OpenCode providerlib/src/runner/runner_support.dart: prompt building, artifact parsing, and shared extraction utilitieslib/src/telegram/telegram_client.dart: Telegram Bot API clientlib/src/session/session_store.dart: in-memory per-chat thread statelib/src/models/telegram_models.dart: Telegram API modelsexample/main.dart: package exampletest/: unit tests.github/workflows/ci.yml: CI pipeline
Development
Run the standard Dart checks locally:
dart analyze
dart test
A GitHub Actions CI pipeline runs dart analyze --fatal-infos and dart test on every push and pull request to main.
License
MIT. See LICENSE.