npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

opencode-telegram-question

v0.5.0

Published

Mirror opencode's question tool to a Telegram bot so you can answer from anywhere

Readme

opencode-telegram-question

Mirror opencode's built-in question tool and permission prompts to a Telegram bot. When opencode pauses for input while you're AFK, answer it from your phone. The CLI session keeps working as if you'd answered locally.

For a higher-quality version with scrubbing, open docs/demo.mp4.

The plugin does not patch the opencode binary. It is a regular plugin that hooks into opencode's bus events and the public SDK client; it should keep working across minor opencode upgrades.

How it works

Questions

  1. opencode's question tool publishes question.asked on the internal bus with the full request (including sub-questions, options, multiple and custom flags).
  2. This plugin's event hook receives that event, sends one Telegram message per sub-question, with inline-keyboard buttons for each option plus a "Type your own answer" entry. Each message is formatted with Telegram HTML (bold header, italic session label and "Recent context" prefix) and labeled with the source session's title so it's obvious which opencode session asked when several sessions share the bot. The first message also includes a short transcript of the last few session messages for context (text, reasoning, and tool titles). The most recent message gets a generous budget shown from its tail (the end of the last message usually carries the detail the question is about); older messages are kept short as breadcrumbs. There is intentionally no "Cancel" button: rejecting a question propagates as a tool error, and a misclick on a phone keyboard shouldn't be able to kill an in-flight request. To cancel, reject from the CLI/TUI; the plugin will then delete its stale Telegram messages via question.rejected.
  3. When you tap a button (or send a free-text reply), the plugin assembles the answer array and calls POST /question/{id}/reply via the SDK client. opencode unblocks the tool and the session continues. The Telegram message is edited in place: chosen options are marked with (check) and the inline keyboard is removed, so you keep a record of what you answered.
  4. If you answer in the CLI/TUI instead, the bus emits question.replied (or question.rejected), and the plugin deletes its Telegram messages so you don't see stale buttons.

Permissions

When a tool needs your approval, opencode publishes permission.asked. The plugin renders the tool name, requested patterns, and any metadata as a chat message with three inline buttons: Allow once, Always allow, Reject. Tapping a button calls POST /permission/{id}/reply and the blocked tool resumes. As with questions, CLI/TUI approvals delete the now-stale chat message via the permission.replied event.

Free text and the "Draft:" indicator

Free-text answers use Telegram's force_reply, so tapping "Type your own answer" pops the reply composer pre-quoted to the question. Concurrent custom prompts (one per sub-question) are routed back to the correct slot via reply_to_message_id.

A side effect of force_reply is that Telegram clients mark the chat with "Draft:" in the chat list as soon as the prompt is sent, even before you type anything. The Bot API offers no method to clear that indicator; the plugin minimizes it by (a) only arming force_reply after you tap "Type your own answer", and (b) deleting the force-reply prompt as soon as the question resolves. Inline-button-only flows (questions without custom, and all permission prompts) never trigger the indicator.

Multi-question calls are supported: every sub-question gets its own message, and the plugin only replies to opencode once all sub-questions have been answered (preserving order).

Long free-text replies are coalesced. Telegram clients split messages longer than 4096 characters into multiple sends, each marked as a reply to the same prompt. The plugin buffers all such chunks, waits for a brief idle window (1.5s by default), then joins them in message_id order and submits a single combined answer. The window is configurable via the freeTextDebounceMs option on the controller; the default is a safe choice for typical reply latency.

You can configure a list of stock free-text answers via quickReplies. Each appears as an extra one-tap button below the options on every question; tapping it submits the configured string as the answer. This is useful for recurring responses like "decide yourself", "skip", or "ask later" without having to type them every time.

Setup

1. Create a Telegram bot

  1. Open @BotFather in Telegram and run /newbot. Follow the prompts to choose a name and username. BotFather will reply with a bot token that looks like 123456:AA.... Save it.
  2. Open the chat with your new bot and send it any message (e.g. /start). This makes the bot able to message you (Telegram blocks unsolicited outbound messages otherwise).

2. Find your chat id

Open @userinfobot in Telegram and send it any message. It will reply with your numeric user id, which is what you pass as chatId for a private 1:1 chat with your own bot.

3. Install the plugin

Add the package name to ~/.config/opencode/opencode.json:

{
  "plugin": [
    [
      "opencode-telegram-question",
      {
        "botToken": "123456:AA...",
        "chatId": 987654321,
        "historyMessages": 3,
        "quickReplies": [
          "Decide yourself. Continue with all remaining tasks and gaps without checkpoints; choose the order. Take decisions autonomously.",
          "Skip this one and move on."
        ]
      }
    ]
  ]
}

opencode will install it automatically from npm on first run.

Why not github:m0wer/...? opencode installs github: plugins by having npm's arborist git-clone the repo on startup. That clone takes ~4s, but short-lived invocations (e.g. opencode models) dispose the instance before it finishes, so arborist rolls back the partial install and the cache is left empty on every launch. An npm tarball install has no clone and loads reliably.

4. Credentials

You can either pass them inline in opencode.json (as shown above) or through the environment. Inline wins if both are set.

export TELEGRAM_BOT_TOKEN="123456:AA..."
export TELEGRAM_CHAT_ID="987654321"

Options:

| Key | Env fallback | Default | Notes | |---|---|---|---| | botToken | TELEGRAM_BOT_TOKEN | (required) | From @BotFather | | chatId | TELEGRAM_CHAT_ID | (required) | Your numeric user id from @userinfobot | | historyMessages | OPENCODE_TELEGRAM_HISTORY | 3 | Lines of recent history prepended to the first sub-question | | 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. | | 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. |

If either credential is missing the plugin disables itself (with a warning) and the CLI/TUI flow is unchanged.

Security notes

  • The plugin only accepts callback queries and messages from the configured chatId. Updates from other chats are ignored.
  • The bot token grants full control of the bot; treat it like a password.

Multiple opencode sessions

Telegram only allows a single concurrent long-poll per bot token, so the plugin coordinates across opencode processes on the same machine via a local IPC endpoint (Unix domain socket on Linux/macOS, named pipe on Windows). The first process to start becomes the leader and runs the poller; later sessions connect as followers and receive updates over the socket. If the leader exits, the followers race to take over and one of them becomes the new leader. Each process still issues its own outbound Telegram calls (sendMessage, editMessage, deleteMessage); only the inbound update stream is shared.

A consequence is that free-text replies are only consumed when the user uses Telegram's Reply gesture against the plugin's force-reply prompt (which the Telegram client triggers automatically when the user taps the pre-filled reply). Stray messages typed into the chat are ignored.

Development

bun install
bun test            # unit + integration tests against an in-memory transport
bun run typecheck
bun run build

Tests cover: single-choice, multi-choice, free-text with force_reply, quick-reply buttons (rendering, submit, prompt cleanup, range checks), concurrent custom-prompt routing via reply_to_message, split-chunk coalescing for long free-text replies (in-order and out-of-order delivery), CLI-resolves-during-buffer cancellation, multi sub-question ordering, cancel/reject, CLI-answers-first cleanup, message edit-on-answer behavior, chat-id isolation, multi-session IPC leader election and broadcast, history part summarization (text/reasoning/tool titles), and permission allow-once/always/reject flows.

Publishing

Publishing is done locally using npm's browser-based OAuth login (no long-lived tokens required).

  1. Bump the version in package.json.
  2. Commit and tag:
    git add package.json
    git commit -m "chore: release v0.x.y"
    git tag v0.x.y
  3. Log in to npm (opens a browser tab for one-time OAuth):
    npm login --auth-type=web
  4. Build and publish:
    npm run prepublishOnly   # typecheck + test + build
    npm publish
  5. Push the commit and tag:
    git push origin master --tags