{"id":50733421,"url":"https://github.com/m0wer/opencode-telegram-question","last_synced_at":"2026-06-10T11:01:42.811Z","repository":{"id":360667380,"uuid":"1248881836","full_name":"m0wer/opencode-telegram-question","owner":"m0wer","description":"Answer OpenCode questions from your phone (remotely through Telegram)","archived":false,"fork":false,"pushed_at":"2026-05-27T10:08:42.000Z","size":4439,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T12:08:04.993Z","etag":null,"topics":["ai","opencode","opencode-plugin","telegram"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/m0wer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T06:23:22.000Z","updated_at":"2026-05-27T10:08:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/m0wer/opencode-telegram-question","commit_stats":null,"previous_names":["m0wer/opencode-telegram-question"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/m0wer/opencode-telegram-question","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m0wer%2Fopencode-telegram-question","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m0wer%2Fopencode-telegram-question/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m0wer%2Fopencode-telegram-question/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m0wer%2Fopencode-telegram-question/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/m0wer","download_url":"https://codeload.github.com/m0wer/opencode-telegram-question/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m0wer%2Fopencode-telegram-question/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34149132,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","opencode","opencode-plugin","telegram"],"created_at":"2026-06-10T11:01:40.739Z","updated_at":"2026-06-10T11:01:42.798Z","avatar_url":"https://github.com/m0wer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# opencode-telegram-question\n\nMirror opencode's built-in `question` tool and permission prompts to a\nTelegram bot. When opencode pauses for input while you're AFK, answer it\nfrom your phone. The CLI session keeps working as if you'd answered\nlocally.\n\n\u003cp align=\"left\"\u003e\n  \u003cimg src=\"docs/demo.webp\" alt=\"Demo: a Telegram chat receives an opencode question, the user taps an option, and the opencode TUI on the desktop picks up the answer in real time.\" width=\"50%\"\u003e\n\u003c/p\u003e\n\nFor a higher-quality version with scrubbing, open [docs/demo.mp4](docs/demo.mp4).\n\nThe plugin does **not** patch the opencode binary. It is a regular plugin\nthat hooks into opencode's bus events and the public SDK client; it should\nkeep working across minor opencode upgrades.\n\n## How it works\n\n### Questions\n\n1. opencode's `question` tool publishes `question.asked` on the internal bus\n   with the full request (including sub-questions, options, `multiple` and\n   `custom` flags).\n2. This plugin's `event` hook receives that event, sends one Telegram\n   message per sub-question, with inline-keyboard buttons for each option\n   plus a \"Type your own answer\" entry. Each message is formatted with\n   Telegram HTML (bold header, italic session label and \"Recent context\"\n   prefix) and labeled with the source session's title so it's obvious\n   which opencode session asked when several sessions share the bot. The\n   first message also includes a short transcript of the last few session\n   messages for context (text, reasoning, and tool titles). The most recent\n   message gets a generous budget shown from its tail (the end of the last\n   message usually carries the detail the question is about); older messages\n   are kept short as breadcrumbs. There is\n   intentionally no \"Cancel\"\n   button: rejecting a question propagates as a tool error, and a misclick\n   on a phone keyboard shouldn't be able to kill an in-flight request. To\n   cancel, reject from the CLI/TUI; the plugin will then delete its stale\n   Telegram messages via `question.rejected`.\n3. When you tap a button (or send a free-text reply), the plugin assembles\n   the answer array and calls `POST /question/{id}/reply` via the SDK\n   client. opencode unblocks the tool and the session continues. The\n   Telegram message is edited in place: chosen options are marked with\n   `(check)` and the inline keyboard is removed, so you keep a record of\n   what you answered.\n4. If you answer in the CLI/TUI instead, the bus emits `question.replied`\n   (or `question.rejected`), and the plugin deletes its Telegram messages\n   so you don't see stale buttons.\n\n### Permissions\n\nWhen a tool needs your approval, opencode publishes `permission.asked`.\nThe plugin renders the tool name, requested patterns, and any metadata as\na chat message with three inline buttons: **Allow once**, **Always\nallow**, **Reject**. Tapping a button calls `POST /permission/{id}/reply`\nand the blocked tool resumes. As with questions, CLI/TUI approvals\ndelete the now-stale chat message via the `permission.replied` event.\n\n### Free text and the \"Draft:\" indicator\n\nFree-text answers use Telegram's `force_reply`, so tapping \"Type your own\nanswer\" pops the reply composer pre-quoted to the question. Concurrent\ncustom prompts (one per sub-question) are routed back to the correct\nslot via `reply_to_message_id`.\n\nA side effect of `force_reply` is that Telegram clients mark the chat\nwith \"Draft:\" in the chat list as soon as the prompt is sent, even\nbefore you type anything. The Bot API offers no method to clear that\nindicator; the plugin minimizes it by (a) only arming `force_reply`\n*after* you tap \"Type your own answer\", and (b) deleting the force-reply\nprompt as soon as the question resolves. Inline-button-only flows\n(questions without `custom`, and all permission prompts) never trigger\nthe indicator.\n\nMulti-question calls are supported: every sub-question gets its own\nmessage, and the plugin only replies to opencode once all sub-questions\nhave been answered (preserving order).\n\nLong free-text replies are coalesced. Telegram clients split messages\nlonger than 4096 characters into multiple sends, each marked as a reply\nto the same prompt. The plugin buffers all such chunks, waits for a\nbrief idle window (1.5s by default), then joins them in `message_id`\norder and submits a single combined answer. The window is configurable\nvia the `freeTextDebounceMs` option on the controller; the default is a\nsafe choice for typical reply latency.\n\nYou can configure a list of stock free-text answers via `quickReplies`.\nEach appears as an extra one-tap button below the options on every\nquestion; tapping it submits the configured string as the answer. This\nis useful for recurring responses like \"decide yourself\", \"skip\", or\n\"ask later\" without having to type them every time.\n\n## Setup\n\n### 1. Create a Telegram bot\n\n1. Open [@BotFather](https://t.me/BotFather) in Telegram and run `/newbot`.\n   Follow the prompts to choose a name and username. BotFather will reply\n   with a **bot token** that looks like `123456:AA...`. Save it.\n2. Open the chat with your new bot and send it any message (e.g. `/start`).\n   This makes the bot able to message you (Telegram blocks unsolicited\n   outbound messages otherwise).\n\n### 2. Find your chat id\n\nOpen [@userinfobot](https://t.me/userinfobot) in Telegram and send it any\nmessage. It will reply with your numeric user id, which is what you pass\nas `chatId` for a private 1:1 chat with your own bot.\n\n### 3. Install the plugin\n\nAdd it to `~/.config/opencode/opencode.json`:\n\n```jsonc\n{\n  \"plugin\": [\n    [\n      \"github:m0wer/opencode-telegram-question\",\n      {\n        \"botToken\": \"123456:AA...\",\n        \"chatId\": 987654321,\n        \"historyMessages\": 3,\n        \"quickReplies\": [\n          \"Decide yourself. Continue with all remaining tasks and gaps without checkpoints; choose the order. Take decisions autonomously.\",\n          \"Skip this one and move on.\"\n        ]\n      }\n    ]\n  ]\n}\n```\n\nopencode resolves the spec through npm, which fetches the repo straight from\nGitHub. Pin a specific commit or tag with `github:m0wer/opencode-telegram-question#v0.4.1`.\n\nFor local development, clone and reference the built file directly:\n\n```bash\ngit clone https://github.com/m0wer/opencode-telegram-question.git\ncd opencode-telegram-question\nbun install\nbun run build\n```\n\n```jsonc\n{\n  \"plugin\": [\n    [\n      \"/absolute/path/to/opencode-telegram-question/dist/index.js\",\n      { \"botToken\": \"...\", \"chatId\": 0 }\n    ]\n  ]\n}\n```\n\n### 4. Credentials\n\nYou can either pass them inline in `opencode.json` (as shown above) or\nthrough the environment. Inline wins if both are set.\n\n```bash\nexport TELEGRAM_BOT_TOKEN=\"123456:AA...\"\nexport TELEGRAM_CHAT_ID=\"987654321\"\n```\n\nOptions:\n\n| Key | Env fallback | Default | Notes |\n|---|---|---|---|\n| `botToken` | `TELEGRAM_BOT_TOKEN` | (required) | From [@BotFather](https://t.me/BotFather) |\n| `chatId` | `TELEGRAM_CHAT_ID` | (required) | Your numeric user id from [@userinfobot](https://t.me/userinfobot) |\n| `historyMessages` | `OPENCODE_TELEGRAM_HISTORY` | `3` | Lines of recent history prepended to the first sub-question |\n| `quickReplies` | `OPENCODE_TELEGRAM_QUICK_REPLIES` | `[]` | List of stock free-text answers shown as extra one-tap buttons after the options. Tapping one submits its text as the answer. The env-var form accepts either a JSON array (e.g. `'[\"decide yourself\",\"skip\"]'`) or a comma-separated list. |\n| `logFile` | (n/a) | platform default | Path to the plugin log file. Defaults to `$XDG_STATE_HOME/opencode-telegram-question/plugin.log` (POSIX) or `%LOCALAPPDATA%\\opencode-telegram-question\\plugin.log` (Windows). The plugin never writes to stdout/stderr, so the TUI stays clean; tail this file when debugging. |\n\nIf either credential is missing the plugin disables itself (with a warning)\nand the CLI/TUI flow is unchanged.\n\n## Security notes\n\n- The plugin only accepts callback queries and messages from the configured\n  `chatId`. Updates from other chats are ignored.\n- The bot token grants full control of the bot; treat it like a password.\n\n## Multiple opencode sessions\n\nTelegram only allows a single concurrent long-poll per bot token, so the\nplugin coordinates across opencode processes on the same machine via a\nlocal IPC endpoint (Unix domain socket on Linux/macOS, named pipe on\nWindows). The first process to start becomes the leader and runs the\npoller; later sessions connect as followers and receive updates over the\nsocket. If the leader exits, the followers race to take over and one of\nthem becomes the new leader. Each process still issues its own outbound\nTelegram calls (sendMessage, editMessage, deleteMessage); only the\ninbound update stream is shared.\n\nA consequence is that free-text replies are only consumed when the user\nuses Telegram's Reply gesture against the plugin's force-reply prompt\n(which the Telegram client triggers automatically when the user taps the\npre-filled reply). Stray messages typed into the chat are ignored.\n\n## Development\n\n```bash\nbun install\nbun test            # unit + integration tests against an in-memory transport\nbun run typecheck\nbun run build\n```\n\nTests cover: single-choice, multi-choice, free-text with `force_reply`,\nquick-reply buttons (rendering, submit, prompt cleanup, range checks),\nconcurrent custom-prompt routing via `reply_to_message`, split-chunk\ncoalescing for long free-text replies (in-order and out-of-order\ndelivery), CLI-resolves-during-buffer cancellation, multi sub-question\nordering, cancel/reject, CLI-answers-first cleanup, message edit-on-answer\nbehavior, chat-id isolation, multi-session IPC leader election and\nbroadcast, history part summarization (text/reasoning/tool titles), and\npermission allow-once/always/reject flows.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fm0wer%2Fopencode-telegram-question","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fm0wer%2Fopencode-telegram-question","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fm0wer%2Fopencode-telegram-question/lists"}