Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Codegram

Bidirectional Telegram bridge for opencode.

Get notified when opencode finishes responding, reply from your phone, route replies to specific conversations, and approve tool requests — all without leaving Telegram.

Features

  • Recap notifications: opencode sends a plain-text summary to Telegram after each response (not raw code)
  • Quote-reply routing: reply to a specific Telegram message to route your reply to that specific conversation
  • Plain-reply fallback: a non-quote reply routes to the most recently idle session
  • Conversation titles: each Telegram message is prefixed with the session title so you know which conversation you're looking at
  • Approval routing: approve/reject tool permission requests from Telegram (y/yes/n/no)
  • Question routing: answer the model's clarification questions from Telegram (number or label)
  • Multi-process safe: works across multiple opencode windows/projects via shared file-based state
  • Secret filtering: skips messages containing common secret patterns (API keys, tokens, private keys)
  • Full response on reply: when you reply via Telegram, the full AI response is sent back (not just the recap)

Quick start

1. Create a Telegram bot

  1. Message @BotFather on Telegram
  2. Send /newbot and follow the prompts
  3. Save the bot token (looks like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11)

2. Get your chat ID

  1. Send any message to your new bot
  2. Open https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates in a browser
  3. Find "chat":{"id":XXXXXXXXX} in the response — that's your chat ID

3. Set environment variables

Add to your shell profile (~/.zshrc, ~/.bashrc, etc.):

export TELEGRAM_BOT_TOKEN="your_bot_token_here"
export TELEGRAM_CHAT_ID="your_chat_id_here"

Reload your shell: source ~/.zshrc (or restart terminal).

4. Install the plugin

Option A — install script:

git clone <repo-url> codegram
cd codegram
node scripts/install.js

Option B — manual copy:

cp telegram-notify.js ~/.config/opencode/plugins/telegram-notify.js

opencode auto-discovers plugins in ~/.config/opencode/plugins/.

5. Restart opencode

Quit all opencode windows and relaunch. The plugin loads on startup — config is not hot-reloaded.

6. Test

Send any prompt to opencode in the TUI. When the AI finishes, you'll get a recap on Telegram. Reply to it — your reply goes back into that conversation.

Configuration

All configuration is via environment variables:

Variable Required Default Description
TELEGRAM_BOT_TOKEN Yes Bot token from @BotFather
TELEGRAM_CHAT_ID Yes Your Telegram chat ID
TELEGRAM_DEBUG No unset Set to 1 to write trace to /tmp/telegram-notify-debug.log
TELEGRAM_ALLOW_SECRETS No unset Set to 1 to disable the secret filter (not recommended)
TELEGRAM_STATE_DIR No OS tmpdir Override the shared state directory

Unconfigured (missing env vars) = silent no-op. The plugin does nothing without both TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.

How it works

Outbound (opencode → Telegram)

When opencode finishes responding (session.idle event), the plugin fetches the last assistant message and looks for a [[RECAP]]...[[/RECAP]] block. This block is injected into the system prompt via experimental.chat.system.transform — the model is instructed to end every response with a plain-text recap.

For normal TUI prompts: only the recap is sent to Telegram (safe, no code).

For Telegram-injected prompts (you replied via Telegram): the full AI response is sent (truncated to 4000 chars, secret-filtered), so you can see the answer without opening the TUI.

Each outbound message is prefixed with the conversation title:

Session naming request

Routed the telegram reply to the correct session...

Inbound (Telegram → opencode)

Every opencode process runs a getUpdates long-poll loop (30s timeout). Telegram 409-throttles to one concurrent poll. Each fetched update is claimed by exactly one process — the shared lastUpdateId in a state file is advanced by 1 under a file lock before handling. This prevents duplicate injection across multiple opencode windows.

Routing:

  • Quote-reply (Telegram's reply feature — drag a message and quote it): routes to the session that originated the quoted message. The outbound Telegram message_id is mapped to the session ID in the shared state file.
  • Plain reply (no quote): routes to the globally last-idle session across all processes.

Injection uses client.session.prompt() — the same API the TUI uses. The message is a real user prompt that triggers a real AI response. It persists in the chat history.

Cross-process state: Routing state lives in a file ($TMPDIR/telegram-notify/state.json), shared across all opencode processes:

  • msgIdToSession: maps outbound Telegram message IDs → session IDs (for quote-reply routing)
  • lastSessionId: the most recently idle session (for plain-reply fallback)
  • lastUpdateId: the Telegram getUpdates offset (for claim-once dedup)

All writes are guarded by a file lock (state.lock) with stale-lock recovery. Reads use atomic file rename to avoid torn JSON.

Approval and question routing

When opencode asks for permission (tool approval) or asks a clarification question:

  1. The plugin sends a notification to Telegram with the tool name/targets or the question + numbered options
  2. Your reply is interpreted:
    • Permission: y/yes/approve/accept → allow once; n/no/disapprove/reject → reject
    • Question: a number (1, 2) or the option label → routed as the answer
  3. Unrecognized replies get a help message listing valid options

"always" is intentionally not wired — persistent approvals from a chat bot are a security risk.

Trace logging

The plugin always writes routing decisions to $TMPDIR/telegram-notify/trace.log — no env var needed. This includes:

  • Inbound message routing (quoted ID, mapped session, target)
  • Inject success/failure (with retry attempts)
  • Toast notifications

For full debug output (including fetch retries), set TELEGRAM_DEBUG=1 and check /tmp/telegram-notify-debug.log.

Limitations

Due to opencode's architecture, the following limitations apply:

TUI does not live-update for unfocused windows

When a Telegram reply is injected into a session, the TUI window showing that session does not re-render if it's not the focused window. The message and AI response are written to the shared SQLite database and persist in chat history, but they won't be visible in the TUI until you navigate to that session.

Workaround: the plugin sends the full AI response back to Telegram for injected prompts, so you see the answer without needing the TUI. A toast notification tells you which session received the reply. Navigate to that session manually when you want the TUI context.

No API to query which session each TUI window is showing

There is no opencode API to determine which session is active in each TUI window. This means:

  • We cannot target a specific window for display
  • We cannot check if the target session is already open in some window before acting
  • tui.session.select broadcasts to the calling process's TUI, but since we can't identify which process has the target session, using it risks hijacking the wrong window

Decision: the plugin does not call tui.session.select. It relies on Telegram for the response + toast for notification. This avoids the "lost session" problem where a window's active session gets unexpectedly replaced.

Approval routing is per-process, not cross-process

The pending approval/question slot is per-process (in-memory). If process A receives a permission.asked event and you reply via Telegram, the reply is handled by whichever process claims the Telegram update — which may not be process A. Cross-process approval routing is not implemented because approval queues are per-server (in-memory request queues).

Workaround: if the Telegram reply doesn't reach the right process, approve/reject in the TUI directly. In practice, the process that asked is usually the one polling Telegram.

Multiple opencode processes share one bot

All opencode processes use the same TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID. There is no per-window Telegram chat. This is by design — the quote-reply routing distinguishes conversations by the quoted message, not by chat.

Secret filter is regex-based, not exhaustive

The secret filter covers common patterns (OpenAI keys, Anthropic keys, Telegram bot tokens, Slack tokens, GitHub tokens, Google API keys, private keys). It will not catch every possible secret format. Do not send sensitive code or credentials through the bot. The recap instruction explicitly forbids code/secrets, but the model may not always comply.

Bot chats are not end-to-end encrypted

Telegram bot chats are not E2E encrypted. The recap is a model-authored summary (safer than raw code), but anyone with the bot token can send injectable prompts. This is the same trust level as the token itself — keep it private.

No per-session notification muting

All sessions across all projects send recaps to the same Telegram chat. There is no way to mute specific sessions or projects. The plugin fires in every chat/session globally.

Telegram message length limit

The full AI response for Telegram-injected prompts is truncated to 4000 characters (Telegram's limit is 4096; the margin accounts for the title prefix). Long responses will be cut off. The recap (for normal prompts) is typically short and unaffected.

File structure

codegram/
├── telegram-notify.js    # The plugin (single file, no dependencies)
├── package.json
├── scripts/
│   └── install.js        # Copies the plugin to ~/.config/opencode/plugins/
└── README.md            # This file

Uninstall

rm ~/.config/opencode/plugins/telegram-notify.js

Remove the env vars from your shell profile. Restart opencode.

License

MIT

About

Bidirectional Telegram bridge for opencode.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages