Problem
There is currently no update-safe way to add a small bot-native Telegram command that executes local code without sending a prompt to OpenCode.
My concrete use case is a /quota command that:
- calls a local quota API or executable;
- displays provider limits directly in Telegram;
- optionally provides a Refresh button;
- does not create an OpenCode session message;
- does not invoke an LLM or consume model tokens.
Editing the installed npm package works only temporarily because the changes are replaced when the bot is updated. Maintaining a permanent fork also seems excessive for a small local integration.
The existing /commands feature does not cover this case because it executes OpenCode custom commands through session.command. This proposal is specifically about native Telegram handlers implemented locally by the bot.
This is also different from forwarding arbitrary unknown slash commands, which was discussed in #56.
Current architecture
From reviewing the current main branch:
- bot commands are defined centrally in
src/bot/commands/definitions.ts;
- handlers are registered in
src/bot/routers/command-router.ts;
- callbacks are registered separately through the callback router;
BOT_COMMANDS is used for Telegram setMyCommands;
- authentication and the interaction guard run before the command routers;
- the bot already has a persistent application directory through
getRuntimePaths() and OPENCODE_TELEGRAM_HOME.
Because the application directory survives npm updates, it could also contain trusted local extensions.
Proposal
Add a minimal, opt-in loader for trusted local .mjs extensions.
For example:
<appHome>/extensions/*.mjs
Typical Linux location:
~/.config/opencode-telegram-bot/extensions/
An extension could export a small manifest and a setup function:
export default {
apiVersion: 1,
id: "omniroute-quota",
commands: [
{
command: "quota",
description: "Show provider quota limits",
allowWhenBusy: true,
},
],
setup({ composer, logger }) {
composer.command("quota", async (ctx) => {
const text = await loadQuotaStatus();
await ctx.reply(text);
});
composer.callbackQuery(
"ext:omniroute-quota:refresh",
async (ctx) => {
const text = await loadQuotaStatus();
await ctx.editMessageText(text);
await ctx.answerCallbackQuery();
},
);
},
};
The exact API is open for discussion. The important requirements are:
- extension files live outside the installed npm package;
- extensions can register native Telegram commands;
- extensions can register namespaced callback handlers;
- extension commands are included in
setMyCommands;
- handlers remain behind the existing authorization middleware;
- one broken extension does not prevent the bot from starting;
- built-in commands cannot be overridden;
- the API is versioned from the beginning.
Suggested MVP boundaries
To keep the first implementation small, the MVP does not need:
- hot reload;
- an extension marketplace;
- automatic extension downloads;
- dependency installation;
- sandboxing;
- project-local extensions;
- a complex lifecycle API;
- compatibility with arbitrary npm plugin packages.
Restarting the bot after adding or changing an extension is sufficient.
Extensions should be documented as fully trusted local code running with the same OS permissions as the bot.
Interaction guard
Some local read-only commands, such as /quota, should be usable while an OpenCode task is running.
An extension command could explicitly request this:
{
command: "quota",
description: "Show provider quota limits",
allowWhenBusy: true,
}
The default should remain false.
Possible integration points
A minimal implementation would likely involve:
- adding
extensionsDirPath to src/runtime/paths.ts;
- loading extension manifests before
createBot();
- merging extension command definitions with the built-in command list;
- mounting extension composers after authentication and the interaction guard;
- mounting them before unknown-command and unknown-callback fallbacks;
- allowing explicitly marked commands through the busy-state guard;
- adding loader, collision and failure-isolation tests.
Backward compatibility
When the extensions directory does not exist or is empty, the bot should behave exactly as it does today.
Done criteria
I would be willing to work on a PR after confirming that this direction fits the project.
Problem
There is currently no update-safe way to add a small bot-native Telegram command that executes local code without sending a prompt to OpenCode.
My concrete use case is a
/quotacommand that:Editing the installed npm package works only temporarily because the changes are replaced when the bot is updated. Maintaining a permanent fork also seems excessive for a small local integration.
The existing
/commandsfeature does not cover this case because it executes OpenCode custom commands throughsession.command. This proposal is specifically about native Telegram handlers implemented locally by the bot.This is also different from forwarding arbitrary unknown slash commands, which was discussed in #56.
Current architecture
From reviewing the current
mainbranch:src/bot/commands/definitions.ts;src/bot/routers/command-router.ts;BOT_COMMANDSis used for TelegramsetMyCommands;getRuntimePaths()andOPENCODE_TELEGRAM_HOME.Because the application directory survives npm updates, it could also contain trusted local extensions.
Proposal
Add a minimal, opt-in loader for trusted local
.mjsextensions.For example:
Typical Linux location:
An extension could export a small manifest and a setup function:
The exact API is open for discussion. The important requirements are:
setMyCommands;Suggested MVP boundaries
To keep the first implementation small, the MVP does not need:
Restarting the bot after adding or changing an extension is sufficient.
Extensions should be documented as fully trusted local code running with the same OS permissions as the bot.
Interaction guard
Some local read-only commands, such as
/quota, should be usable while an OpenCode task is running.An extension command could explicitly request this:
The default should remain
false.Possible integration points
A minimal implementation would likely involve:
extensionsDirPathtosrc/runtime/paths.ts;createBot();Backward compatibility
When the extensions directory does not exist or is empty, the bot should behave exactly as it does today.
Done criteria
.mjsextensions from its persistent application directory.setMyCommands.apiVersionvalues are rejected with a clear log message.I would be willing to work on a PR after confirming that this direction fits the project.