OpenRouter is used when no provider flag is set. Claude is selected with .cl, and GLM stays available via .glm.
8.8 KiB
AGENTS.md
Repository guidance for automated coding tools and contributors.
Running the bot
uv sync
uv run python -m playwright install chromium
uv run python main.py
Do not use crawl4ai-setup in unprivileged containers such as Pterodactyl: it invokes Playwright with --with-deps and attempts to become root. Installing Chromium with the command above only writes to the current user's Playwright cache. If Chromium reports missing shared libraries at runtime, they must be included in the Docker image by its administrator.
Logs go to bot.log by default (rotating, 10 MB, 3 backups). Override with LOG_FILE=<path>.
Run the test suite with:
uv run python -m unittest discover -s tests -v
Configuration
Copy config.example.toml to config.toml and fill in credentials, or use environment variables (.env.example shows the env-var form). Both sources are merged at startup — env vars take precedence.
Key env vars: TG_API_ID, TG_API_HASH, SESSION_DIR, ACCOUNTS_JSON (JSON list) or ACCOUNT_SESSIONS (comma-separated session names), BOT_CONFIG (path to config file), and BOT_DOTENV_PATH (path to .env).
Set use_qr = true on an account to authorize a new user session by scanning the QR code printed in the terminal. Existing authorized sessions start normally. If the account has 2FA enabled, Kurigram asks for its password in the terminal after the QR code is scanned.
The .txt transcription command uses [elevenlabs] settings or ELEVENLABS_API_KEY, ELEVENLABS_MODEL_ID, ELEVENLABS_LANGUAGE_CODE, and ELEVENLABS_REQUEST_TIMEOUT_SECONDS.
The .ai agent command uses [ai.openrouter] when no provider flag is given. [ai.glm] is selected with .glm, [ai.deepseek] with .ds, [ai.claude] with .cl (or .claude), and [ai.openrouter] explicitly with .or (or .openrouter). If several provider flags are present, the last one wins. Provider environment variables are GLM_API_URL, GLM_API_KEY, GLM_MODEL, DEEPSEEK_API_URL, DEEPSEEK_API_KEY, DEEPSEEK_MODEL, CLAUDE_API_URL, CLAUDE_API_KEY, CLAUDE_MODEL, OPENROUTER_API_URL, OPENROUTER_API_KEY, and OPENROUTER_MODEL. Z.AI GLM uses the Chat Completions protocol; DeepSeek uses the Responses protocol; Claude uses the Anthropic Messages API; OpenRouter uses its OpenAI-compatible Chat Completions API. The default Claude model is claude-haiku-5-5. The default OpenRouter model is anthropic/claude-haiku-5.5. .th enables reasoning. For Claude, .th sends adaptive thinking with summarized display; without it, thinking is disabled. For OpenRouter, .th sends reasoning.effort=medium and later tool turns echo reasoning_details. Brave Search is available in every AI session and requires BRAVE_SEARCH_API_KEY or ai.brave_api_key. Crawl4AI's read_web_page tool is also always available and uses headless Playwright Chromium to render public pages into Markdown before analysis. Configure it with ai.web_page_timeout_seconds / AI_WEB_PAGE_TIMEOUT_SECONDS and ai.web_page_max_chars / AI_WEB_PAGE_MAX_CHARS.
AI prompt templates are always loaded from MASTER.txt and USER.txt in ai.prompt_dir (or AI_PROMPT_DIR) for every invocation. The directory defaults to prompts; relative paths are resolved from the repository root. C-style /* ... */ blocks and // comments (at line start or following whitespace) are removed before rendering. Both {name} and {{name}} placeholders are supported, including dotted values such as caller_user.id, caller_user.name, owner.id, owner.name, is_owner, text, chat.*, message.*, reply.*, account.*, provider, thinking, and context_messages. Unknown placeholders fail the command with 👎.
Architecture
Boot path
main.py → load_app_config() → AccountManager → one Pyrogram (kurigram) Client per account → register_common_handlers() → pyrogram.idle().
On supported platforms, main.py runs the application with uvloop; Windows automatically falls back to the standard asyncio event loop.
Command system (app/commands/)
- Dispatcher (
dispatcher.py): parses.command(owner prefix) or..command(shared/guest prefix) from message text, resolves to aCommandDefinition, runs pre-checks, calls handler. - Registry (
core.py):CommandRegistryholdsCommandDefinitionobjects.DEFAULT_REGISTRYis the singleton used at runtime; commands self-register via decorator. - Built-in commands (
builtins.py):.ping,.status,.restart,.id,.help,.rights/.acl,.allow,.deny(access),.echo(shared),.txt(shared ElevenLabs transcription), and.ai(shared agent loop). Add new commands here using the@command_registry.command(...)decorator pattern. - Check system (
core.py):CommandCheckfunctions (e.g.owner_only_check(),shared_access_check(),require_reply_check(),require_args_check()) gate handler execution. Chain them with.check(...)or.owner_only()/.require_reply()/.require_args()on the builder. - ACL (
acl.py):RuntimeAclStorepersists per-session<session>.acl.jsonallow/deny rules. Only shared (.is_shared) commands can be granted to guests.
Transcription (app/transcription.py)
.txt / ..txt must reply to a voice message, video note, or video. The command downloads the Telegram media temporarily and sends it to ElevenLabs Speech to Text. Invalid usage and transcription failures react to the command message with 👎 and send no error reply. Guests need an ACL grant for txt.
Agent loop (app/agentic.py)
.ai [count] [.glm|.ds|.cl|.or] [.th] <prompt> runs micro-agents with OpenRouter by default. .glm selects GLM, .ds selects DeepSeek, .cl selects Claude, .or selects OpenRouter explicitly, and .th enables reasoning. Every session exposes the micro-agents-tools Brave Search tool. Unknown leading dot-arguments are ignored. The optional count adds preceding Telegram messages; a replied message is always injected once. Commands are accepted from either message text or a media caption. Media at or below 5 MiB, including media attached directly to the command message, is attached. Invalid syntax and runtime/API failures only react with 👎. Guests need an ACL grant for ai and invoke it as ..ai.
While an .ai command is running, its Telegram message has a 🤔 reaction. The reaction is removed after a successful answer and replaced with 👎 on failure.
The master and user prompt callbacks must render MASTER.txt and USER.txt from the configured prompt directory; do not add a hard-coded fallback that bypasses those files.
Every historical or replied Telegram message is a UserMessage rendered through USER.txt; message.id, message.text, message.date, message.user.*, and message.is_reply describe that specific message. Filesystem and Python execution tools are registered through AgentToolManager but exposed and executable only when is_owner is true. Telegram send_message, send_image, and send_file tools target the current chat. Owner file sends are confined to the repository root; guest file sends are confined to the current session's temporary media directory to prevent repository data disclosure.
Each account keeps a bounded in-memory AgentSessionStore. Replies to Telegram messages previously emitted as final .ai answers reuse the corresponding micro-agents session. Session reuse is checked only inside the .ai command handler; ordinary replies without .ai/..ai never trigger the agent. All chunks of a long final answer are mapped to the same session, and session workspaces are cleaned up when evicted or when the account manager stops.
Key types
CommandContext— per-invocation context passed to every handler; holdsclient,manager,account,message, parsedargs/argv, and convenience properties (actor_id,owner_id,is_owner,replied_user).AccountState— runtime state per account (client, ACL store, status,meobject).
Adding a new command
@command_registry.command(
"mycommand",
"Short description",
usage=".mycommand <arg>",
category="general",
).shared() # omit for owner-only; add .require_args() / .require_reply() as needed
async def mycommand(ctx: "CommandContext") -> None:
await ctx.reply(ctx.args)
Shared commands use ..mycommand for guests and .mycommand for the owner. Owner-only commands use .mycommand only.
Change guidelines
- Preserve unrelated local and untracked changes.
- Manage dependencies through
pyproject.tomlanduv.lock; useuv syncinstead ofpip install. - Register commands through
command_registry; do not add separate Pyrogram handlers for individual commands. - Keep shared commands behind
shared_access_check()and persist ACL changes throughRuntimeAclStore. - Add or update focused
unittestcoverage for behavioral changes.