opencode-telegram-group-topics-bot 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.env.example +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/agent/manager.js +60 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +47 -0
  7. package/dist/bot/commands/abort.js +116 -0
  8. package/dist/bot/commands/commands.js +389 -0
  9. package/dist/bot/commands/constants.js +20 -0
  10. package/dist/bot/commands/definitions.js +25 -0
  11. package/dist/bot/commands/help.js +27 -0
  12. package/dist/bot/commands/models.js +38 -0
  13. package/dist/bot/commands/new.js +247 -0
  14. package/dist/bot/commands/opencode-start.js +85 -0
  15. package/dist/bot/commands/opencode-stop.js +44 -0
  16. package/dist/bot/commands/projects.js +304 -0
  17. package/dist/bot/commands/rename.js +173 -0
  18. package/dist/bot/commands/sessions.js +491 -0
  19. package/dist/bot/commands/start.js +67 -0
  20. package/dist/bot/commands/status.js +138 -0
  21. package/dist/bot/constants.js +49 -0
  22. package/dist/bot/handlers/agent.js +127 -0
  23. package/dist/bot/handlers/context.js +125 -0
  24. package/dist/bot/handlers/document.js +65 -0
  25. package/dist/bot/handlers/inline-menu.js +124 -0
  26. package/dist/bot/handlers/model.js +152 -0
  27. package/dist/bot/handlers/permission.js +281 -0
  28. package/dist/bot/handlers/prompt.js +263 -0
  29. package/dist/bot/handlers/question.js +285 -0
  30. package/dist/bot/handlers/variant.js +147 -0
  31. package/dist/bot/handlers/voice.js +173 -0
  32. package/dist/bot/index.js +945 -0
  33. package/dist/bot/message-patterns.js +4 -0
  34. package/dist/bot/middleware/auth.js +30 -0
  35. package/dist/bot/middleware/interaction-guard.js +80 -0
  36. package/dist/bot/middleware/unknown-command.js +22 -0
  37. package/dist/bot/scope.js +222 -0
  38. package/dist/bot/telegram-constants.js +3 -0
  39. package/dist/bot/telegram-rate-limiter.js +263 -0
  40. package/dist/bot/utils/commands.js +21 -0
  41. package/dist/bot/utils/file-download.js +91 -0
  42. package/dist/bot/utils/keyboard.js +85 -0
  43. package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
  44. package/dist/bot/utils/session-error-filter.js +34 -0
  45. package/dist/bot/utils/topic-link.js +29 -0
  46. package/dist/cli/args.js +98 -0
  47. package/dist/cli.js +80 -0
  48. package/dist/config.js +103 -0
  49. package/dist/i18n/de.js +330 -0
  50. package/dist/i18n/en.js +330 -0
  51. package/dist/i18n/es.js +330 -0
  52. package/dist/i18n/index.js +102 -0
  53. package/dist/i18n/ru.js +330 -0
  54. package/dist/i18n/zh.js +330 -0
  55. package/dist/index.js +28 -0
  56. package/dist/interaction/cleanup.js +24 -0
  57. package/dist/interaction/constants.js +25 -0
  58. package/dist/interaction/guard.js +100 -0
  59. package/dist/interaction/manager.js +113 -0
  60. package/dist/interaction/types.js +1 -0
  61. package/dist/keyboard/manager.js +115 -0
  62. package/dist/keyboard/types.js +1 -0
  63. package/dist/model/capabilities.js +62 -0
  64. package/dist/model/manager.js +257 -0
  65. package/dist/model/types.js +24 -0
  66. package/dist/opencode/client.js +13 -0
  67. package/dist/opencode/events.js +159 -0
  68. package/dist/opencode/prompt-submit-error.js +101 -0
  69. package/dist/permission/manager.js +92 -0
  70. package/dist/permission/types.js +1 -0
  71. package/dist/pinned/manager.js +405 -0
  72. package/dist/pinned/types.js +1 -0
  73. package/dist/process/manager.js +273 -0
  74. package/dist/process/types.js +1 -0
  75. package/dist/project/manager.js +88 -0
  76. package/dist/question/manager.js +186 -0
  77. package/dist/question/types.js +1 -0
  78. package/dist/rename/manager.js +64 -0
  79. package/dist/runtime/bootstrap.js +350 -0
  80. package/dist/runtime/mode.js +74 -0
  81. package/dist/runtime/paths.js +37 -0
  82. package/dist/runtime/process-error-handlers.js +24 -0
  83. package/dist/session/cache-manager.js +455 -0
  84. package/dist/session/manager.js +87 -0
  85. package/dist/settings/manager.js +283 -0
  86. package/dist/stt/client.js +64 -0
  87. package/dist/summary/aggregator.js +625 -0
  88. package/dist/summary/formatter.js +417 -0
  89. package/dist/summary/tool-message-batcher.js +277 -0
  90. package/dist/topic/colors.js +8 -0
  91. package/dist/topic/constants.js +10 -0
  92. package/dist/topic/manager.js +161 -0
  93. package/dist/topic/title-constants.js +2 -0
  94. package/dist/topic/title-format.js +10 -0
  95. package/dist/topic/title-sync.js +17 -0
  96. package/dist/utils/error-format.js +29 -0
  97. package/dist/utils/logger.js +175 -0
  98. package/dist/utils/safe-background-task.js +33 -0
  99. package/dist/variant/manager.js +103 -0
  100. package/dist/variant/types.js +1 -0
  101. package/package.json +76 -0
package/.env.example ADDED
@@ -0,0 +1,74 @@
1
+ # Telegram Bot Token (from @BotFather)
2
+ TELEGRAM_BOT_TOKEN=
3
+
4
+ # Allowed Telegram User ID (from @userinfobot)
5
+ TELEGRAM_ALLOWED_USER_ID=
6
+
7
+ # Telegram Proxy URL (optional)
8
+ # Supports socks5://, socks4://, http://, https:// protocols
9
+ # Examples:
10
+ # TELEGRAM_PROXY_URL=socks5://proxy.example.com:1080
11
+ # TELEGRAM_PROXY_URL=socks5://user:password@proxy.example.com:1080
12
+ # TELEGRAM_PROXY_URL=http://proxy.example.com:8080
13
+ # TELEGRAM_PROXY_URL=
14
+
15
+ # OpenCode API URL (optional, default: http://localhost:4096)
16
+ # OPENCODE_API_URL=http://localhost:4096
17
+
18
+ # OpenCode Server Authentication (optional)
19
+ # OPENCODE_SERVER_USERNAME=opencode
20
+ # OPENCODE_SERVER_PASSWORD=
21
+
22
+ # OpenCode Model Configuration (REQUIRED)
23
+ # You must specify a default model provider and model ID
24
+ # Examples:
25
+ # Anthropic Claude 3.5 Sonnet: OPENCODE_MODEL_PROVIDER=anthropic, OPENCODE_MODEL_ID=claude-3-5-sonnet-20241022
26
+ # OpenAI GPT-4 Turbo: OPENCODE_MODEL_PROVIDER=openai, OPENCODE_MODEL_ID=gpt-4-turbo
27
+ # Groq Mixtral: OPENCODE_MODEL_PROVIDER=groq, OPENCODE_MODEL_ID=mixtral-8x7b-32768
28
+ OPENCODE_MODEL_PROVIDER=opencode
29
+ OPENCODE_MODEL_ID=big-pickle
30
+
31
+ # Server Configuration (optional)
32
+ # Logging level: debug, info, warn, error (default: info)
33
+ # Use "debug" to see detailed diagnostic logs including all bot events
34
+ # LOG_LEVEL=info
35
+
36
+ # Bot Configuration (optional)
37
+ # Maximum number of sessions shown in /sessions (default: 10)
38
+ # SESSIONS_LIST_LIMIT=10
39
+
40
+ # Maximum number of projects shown in /projects (default: 10)
41
+ # PROJECTS_LIST_LIMIT=10
42
+
43
+ # Bot locale: supported locale code (default: en)
44
+ # Supported locales: en, de, es, ru, zh
45
+ # BOT_LOCALE=en
46
+
47
+ # Service message batching interval in seconds (thinking + tool calls, default: 5)
48
+ # Recommended: keep >=2 to reduce risk of hitting Telegram rate limits (about 1 message/sec)
49
+ # 0 = send immediately (can hit rate limits if there are many tool calls)
50
+ # SERVICE_MESSAGES_INTERVAL_SEC=5
51
+
52
+ # Hide thinking indicator messages (default: false)
53
+ # HIDE_THINKING_MESSAGES=false
54
+
55
+ # Hide tool call service messages (default: false)
56
+ # HIDE_TOOL_CALL_MESSAGES=false
57
+
58
+ # Assistant message formatting mode (default: markdown)
59
+ # markdown = convert assistant replies to Telegram MarkdownV2
60
+ # raw = show assistant replies as plain text
61
+ # MESSAGE_FORMAT_MODE=markdown
62
+
63
+ # Code File Settings (optional)
64
+ # Maximum file size in KB to send as document (default: 100)
65
+ # CODE_FILE_MAX_SIZE_KB=100
66
+
67
+ # Speech-to-Text / Voice Recognition (optional)
68
+ # Enable voice message transcription by setting a Whisper-compatible API URL.
69
+ # Works with OpenAI, Groq, or any Whisper-compatible endpoint.
70
+ # If STT_API_URL is not set, voice messages will get a "not configured" reply.
71
+ # STT_API_URL=
72
+ # STT_API_KEY=
73
+ # STT_MODEL=
74
+ # STT_LANGUAGE=
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruslan Grinev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # OpenCode Telegram Group Topics Bot
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
5
+
6
+ A Telegram bot for [OpenCode](https://opencode.ai) that turns one Telegram supergroup into a multi-session mobile workspace.
7
+
8
+ This project is a fork of the original single-chat bot: [grinev/opencode-telegram-bot](https://github.com/grinev/opencode-telegram-bot) by Ruslan Grinev.
9
+
10
+ - Use the upstream project if you want the simpler single-chat workflow.
11
+ - Use this fork if you want one **General** control topic plus dedicated forum topics for parallel OpenCode sessions.
12
+
13
+ No open ports, no exposed web UI. The bot talks only to your local OpenCode server and the Telegram Bot API.
14
+
15
+ You can run many session topics in parallel inside one group, and many groups in parallel across different projects.
16
+
17
+ Platforms: macOS, Windows, Linux
18
+
19
+ Languages: English (`en`), Deutsch (`de`), Espanol (`es`), Russkiy (`ru`), Jian ti Zhong wen (`zh`)
20
+
21
+ Fork sync notes: [`FORK_SYNC.md`](./FORK_SYNC.md)
22
+
23
+ <p align="center">
24
+ <img src="assets/screencast.gif" width="45%" alt="OpenCode Telegram Group Topics Bot screencast" />
25
+ </p>
26
+
27
+ ## At a Glance
28
+
29
+ - One Telegram group usually maps to one repo / project workspace.
30
+ - The **General** topic is the control lane for `/projects`, `/sessions`, `/new`, and status checks.
31
+ - Each new OpenCode session gets its own forum topic.
32
+ - Each topic keeps its own session, model, agent, and pinned status state.
33
+ - Multiple topics can run at the same time, and multiple groups can be active at the same time.
34
+ - DMs are for light control/status usage, not the main multi-session workflow.
35
+
36
+ ## Quick Start
37
+
38
+ ### 1. Prerequisites
39
+
40
+ - Install **Node.js 20+**
41
+ - Install **OpenCode** from [opencode.ai](https://opencode.ai) or [GitHub](https://github.com/sst/opencode)
42
+ - Create a Telegram bot with [@BotFather](https://t.me/BotFather)
43
+ - Get your Telegram numeric user ID from [@userinfobot](https://t.me/userinfobot)
44
+
45
+ ### 2. Create and Prepare the Telegram Group
46
+
47
+ 1. Create a new Telegram **supergroup** for one OpenCode project/repository.
48
+ 2. Add your bot to that group.
49
+ 3. Make the bot an admin with permission to **Manage Topics**.
50
+ 4. Enable **Topics** in the group settings.
51
+ 5. In [@BotFather](https://t.me/BotFather), run `/setprivacy` for the bot and choose **Disable**.
52
+ 6. Keep the default **General** topic - that is the control lane.
53
+
54
+ ### 3. Start OpenCode
55
+
56
+ Run OpenCode on the machine where the bot will live:
57
+
58
+ ```bash
59
+ opencode serve
60
+ ```
61
+
62
+ Default API URL: `http://localhost:4096`
63
+
64
+ ### 4. Install the Bot
65
+
66
+ #### Option A: `npx`
67
+
68
+ ```bash
69
+ npx opencode-telegram-group-topics-bot
70
+ ```
71
+
72
+ #### Option B: Global install
73
+
74
+ ```bash
75
+ npm install -g opencode-telegram-group-topics-bot
76
+ opencode-telegram-group-topics-bot config
77
+ opencode-telegram-group-topics-bot start
78
+ ```
79
+
80
+ #### Option C: Run from source
81
+
82
+ ```bash
83
+ git clone https://github.com/shanekunz/opencode-telegram-group-topics-bot.git
84
+ cd opencode-telegram-group-topics-bot
85
+ npm install
86
+ npm run build
87
+ node dist/cli.js config --mode sources
88
+ npm run dev
89
+ ```
90
+
91
+ `dist/cli.js` is the compiled CLI entrypoint produced by `npm run build`.
92
+
93
+ ### 5. Complete the Setup Wizard
94
+
95
+ The wizard asks for:
96
+
97
+ - interface language
98
+ - Telegram bot token
99
+ - allowed Telegram user ID
100
+ - OpenCode API URL
101
+ - optional OpenCode server username/password
102
+
103
+ ### 6. First-Time Verification
104
+
105
+ 1. Open a DM with your bot and run `/start`.
106
+ 2. Confirm the bot replies.
107
+ 3. Open your Telegram group and run `/start` in **General**.
108
+ 4. Run `/status` and confirm the bot can reach OpenCode.
109
+ 5. Run `/projects` in **General** and pick the repo for this group.
110
+ 6. Run `/new` in **General** to create a session topic.
111
+ 7. Open the new topic and send a prompt.
112
+
113
+ If that works, your group workspace is ready.
114
+
115
+ ## Daily Workflow
116
+
117
+ 1. Start OpenCode with `opencode serve`
118
+ 2. Start the bot
119
+ 3. Open the Telegram group and go to **General**
120
+ 4. Use `/projects` to confirm the selected repo
121
+ 5. Use `/new` to create a new session topic
122
+ 6. Work inside the topic thread
123
+ 7. Use `/sessions` in **General** to revisit older session lanes
124
+
125
+ ## Parallel Workloads and Telegram Rate Limits
126
+
127
+ - This fork is designed for parallel work: many topic threads in one group, and many groups across projects.
128
+ - Telegram enforces message rate limits, especially when many topics are receiving updates at once.
129
+ - The bot handles those limits gracefully and slows or staggers Telegram updates when needed.
130
+ - Your OpenCode sessions continue running even if Telegram updates become less frequent.
131
+ - In heavy parallel usage, expect less real-time chatter per topic, but not lost OpenCode work.
132
+
133
+ ## Commands
134
+
135
+ | Command | Description |
136
+ | ----------------- | ------------------------------------------------------- |
137
+ | `/status` | Server health, current project, session, and model info |
138
+ | `/new` | Create a new session topic |
139
+ | `/abort` | Abort the current task |
140
+ | `/sessions` | Browse and switch between recent sessions |
141
+ | `/projects` | Switch between OpenCode projects |
142
+ | `/rename` | Rename the current session |
143
+ | `/commands` | Browse and run custom commands |
144
+ | `/opencode_start` | Start the OpenCode server remotely |
145
+ | `/opencode_stop` | Stop the OpenCode server remotely |
146
+ | `/help` | Show available commands |
147
+
148
+ Any normal text message in a session topic is treated as a prompt when no blocking interaction is active.
149
+
150
+ ## How This Fork Differs From Upstream
151
+
152
+ | Topic | Upstream | This fork |
153
+ | -------------- | --------------------------- | --------------------------- |
154
+ | Main UX | One chat | One group with forum topics |
155
+ | Session layout | Switch sessions in one lane | One topic per session lane |
156
+ | Best for | Simplicity | Parallel mobile workflows |
157
+ | Complexity | Lower | Higher |
158
+
159
+ If you want the simpler path, use the upstream project.
160
+
161
+ ## Configuration
162
+
163
+ ### Config Location
164
+
165
+ - Source mode stores config in the repository root.
166
+ - Installed mode stores config in the platform app-data directory.
167
+ - `OPENCODE_TELEGRAM_HOME` overrides both and forces a custom config directory.
168
+
169
+ Installed-mode config paths:
170
+
171
+ - macOS: `~/Library/Application Support/opencode-telegram-group-topics-bot/.env`
172
+ - Windows: `%APPDATA%\opencode-telegram-group-topics-bot\.env`
173
+ - Linux: `~/.config/opencode-telegram-group-topics-bot/.env`
174
+
175
+ ### Environment Variables
176
+
177
+ | Variable | Description | Required | Default |
178
+ | ------------------------------- | ------------------------------------------------------------------------------------ | :------: | ------------------------ |
179
+ | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | - |
180
+ | `TELEGRAM_ALLOWED_USER_ID` | Your numeric Telegram user ID | Yes | - |
181
+ | `TELEGRAM_PROXY_URL` | Proxy URL for Telegram API (SOCKS5/HTTP) | No | - |
182
+ | `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
183
+ | `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
184
+ | `OPENCODE_SERVER_PASSWORD` | Server auth password | No | - |
185
+ | `OPENCODE_MODEL_PROVIDER` | Default model provider | Yes | `opencode` |
186
+ | `OPENCODE_MODEL_ID` | Default model ID | Yes | `big-pickle` |
187
+ | `BOT_LOCALE` | Bot UI language (`en`, `de`, `es`, `ru`, `zh`) | No | `en` |
188
+ | `SESSIONS_LIST_LIMIT` | Sessions per page in `/sessions` | No | `10` |
189
+ | `PROJECTS_LIST_LIMIT` | Projects per page in `/projects` | No | `10` |
190
+ | `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval; keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
191
+ | `HIDE_THINKING_MESSAGES` | Hide `Thinking...` service messages | No | `false` |
192
+ | `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages | No | `false` |
193
+ | `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` or `raw` | No | `markdown` |
194
+ | `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as a document | No | `100` |
195
+ | `STT_API_URL` | Whisper-compatible API base URL | No | - |
196
+ | `STT_API_KEY` | API key for your STT provider | No | - |
197
+ | `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
198
+ | `STT_LANGUAGE` | Optional language hint | No | - |
199
+ | `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
200
+
201
+ Keep your `.env` private. It contains your bot token.
202
+
203
+ ### Optional: Voice and Audio Transcription
204
+
205
+ If `STT_API_URL` and `STT_API_KEY` are set, the bot can transcribe Telegram voice/audio messages before sending them to OpenCode.
206
+
207
+ Whisper-compatible examples:
208
+
209
+ - OpenAI: `https://api.openai.com/v1`
210
+ - Groq: `https://api.groq.com/openai/v1`
211
+ - Together: `https://api.together.xyz/v1`
212
+
213
+ ### Model Picker Notes
214
+
215
+ - Favorites are shown before recent models
216
+ - The current model is marked with `✅`
217
+ - The default model from `OPENCODE_MODEL_PROVIDER` + `OPENCODE_MODEL_ID` is always included
218
+
219
+ To add favorites, open the OpenCode TUI and press `Cmd+F` / `Ctrl+F` on a model.
220
+
221
+ ## Features
222
+
223
+ - Thread-scoped OpenCode sessions inside Telegram forum topics
224
+ - Pinned live status messages per topic
225
+ - Model, agent, variant, and context controls from the keyboard
226
+ - Custom OpenCode command execution
227
+ - Interactive permission and question handling
228
+ - Voice/audio transcription support
229
+ - File attachments for images, PDFs, and text files
230
+ - Strict single-user access control
231
+
232
+ ## Security
233
+
234
+ Only the Telegram user whose ID matches `TELEGRAM_ALLOWED_USER_ID` can use the bot.
235
+
236
+ Since the bot runs locally and connects to your local OpenCode server, there is no exposed public service beyond Telegram itself.
237
+
238
+ ## Development
239
+
240
+ ### Run from source
241
+
242
+ ```bash
243
+ git clone https://github.com/shanekunz/opencode-telegram-group-topics-bot.git
244
+ cd opencode-telegram-group-topics-bot
245
+ npm install
246
+ npm run build
247
+ node dist/cli.js config --mode sources
248
+ npm run dev
249
+ ```
250
+
251
+ ### Scripts
252
+
253
+ | Script | Description |
254
+ | ------------------------------- | ----------------------- |
255
+ | `npm run dev` | Build and start |
256
+ | `npm run build` | Compile TypeScript |
257
+ | `npm start` | Run compiled code |
258
+ | `npm run release:notes:preview` | Preview release notes |
259
+ | `npm run lint` | Run ESLint |
260
+ | `npm run format` | Run Prettier |
261
+ | `npm test` | Run tests |
262
+ | `npm run test:coverage` | Run tests with coverage |
263
+
264
+ No watcher is used because the bot maintains persistent SSE and polling connections.
265
+
266
+ ## Troubleshooting
267
+
268
+ **Bot does not respond**
269
+
270
+ - Confirm `TELEGRAM_ALLOWED_USER_ID` matches your real Telegram user ID
271
+ - Confirm the bot token is correct
272
+ - Make sure you disabled privacy mode in BotFather for group usage
273
+
274
+ **OpenCode server is unavailable**
275
+
276
+ - Make sure `opencode serve` is running
277
+ - Confirm `OPENCODE_API_URL` points to the correct address
278
+
279
+ **Cannot create new session topics**
280
+
281
+ - Confirm the group is a supergroup with Topics enabled
282
+ - Confirm the bot is an admin with **Manage Topics** permission
283
+ - Run `/new` from **General**, not inside an existing session topic
284
+
285
+ **No models appear in the picker**
286
+
287
+ - Add favorites in the OpenCode TUI
288
+ - Confirm `OPENCODE_MODEL_PROVIDER` and `OPENCODE_MODEL_ID` are valid for your setup
289
+
290
+ **Linux permission issues**
291
+
292
+ - Check the CLI binary is executable: `chmod +x $(which opencode-telegram-group-topics-bot)`
293
+ - Check the config directory is writable: `~/.config/opencode-telegram-group-topics-bot/`
294
+
295
+ ## Contributing
296
+
297
+ Please follow [CONTRIBUTING.md](CONTRIBUTING.md).
298
+
299
+ ## Community
300
+
301
+ Open issues in this repository for this fork. For upstream discussion, see [grinev/opencode-telegram-bot](https://github.com/grinev/opencode-telegram-bot).
302
+
303
+ ## License
304
+
305
+ [MIT](LICENSE) © Ruslan Grinev
@@ -0,0 +1,60 @@
1
+ import { opencodeClient } from "../opencode/client.js";
2
+ import { getCurrentProject, getCurrentAgent, setCurrentAgent } from "../settings/manager.js";
3
+ import { getCurrentSession } from "../session/manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ const DEFAULT_AGENT = "build";
6
+ export async function getAvailableAgents(scopeKey = "global") {
7
+ try {
8
+ const project = getCurrentProject(scopeKey);
9
+ const { data: agents, error } = await opencodeClient.app.agents(project ? { directory: project.worktree } : undefined);
10
+ if (error) {
11
+ logger.error("[AgentManager] Failed to fetch agents:", error);
12
+ return [];
13
+ }
14
+ if (!agents) {
15
+ return [];
16
+ }
17
+ return agents.filter((agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"));
18
+ }
19
+ catch (err) {
20
+ logger.error("[AgentManager] Error fetching agents:", err);
21
+ return [];
22
+ }
23
+ }
24
+ export async function fetchCurrentAgent(scopeKey = "global") {
25
+ const storedAgent = getCurrentAgent(scopeKey);
26
+ const session = getCurrentSession(scopeKey);
27
+ const project = getCurrentProject(scopeKey);
28
+ if (!session || !project) {
29
+ return storedAgent ?? DEFAULT_AGENT;
30
+ }
31
+ try {
32
+ const { data: messages, error } = await opencodeClient.session.messages({
33
+ sessionID: session.id,
34
+ directory: project.worktree,
35
+ limit: 1,
36
+ });
37
+ if (error || !messages || messages.length === 0) {
38
+ return storedAgent ?? DEFAULT_AGENT;
39
+ }
40
+ const lastAgent = messages[0].info.agent;
41
+ if (storedAgent && lastAgent !== storedAgent) {
42
+ return storedAgent;
43
+ }
44
+ if (lastAgent && lastAgent !== storedAgent) {
45
+ setCurrentAgent(lastAgent, scopeKey);
46
+ }
47
+ return lastAgent || storedAgent || DEFAULT_AGENT;
48
+ }
49
+ catch (err) {
50
+ logger.error("[AgentManager] Error fetching current agent:", err);
51
+ return storedAgent ?? DEFAULT_AGENT;
52
+ }
53
+ }
54
+ export function selectAgent(agentName, scopeKey = "global") {
55
+ logger.info(`[AgentManager] Selected agent: ${agentName}`);
56
+ setCurrentAgent(agentName, scopeKey);
57
+ }
58
+ export function getStoredAgent(scopeKey = "global") {
59
+ return getCurrentAgent(scopeKey) ?? DEFAULT_AGENT;
60
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Agent emoji mapping for visual distinction
3
+ */
4
+ export const AGENT_EMOJI = {
5
+ plan: "📋",
6
+ build: "🛠️",
7
+ general: "💬",
8
+ explore: "🔍",
9
+ title: "📝",
10
+ summary: "📄",
11
+ compaction: "📦",
12
+ };
13
+ /**
14
+ * Get emoji for agent (fallback to 🤖 if not found)
15
+ */
16
+ export function getAgentEmoji(agentName) {
17
+ return AGENT_EMOJI[agentName] ?? "🤖";
18
+ }
19
+ /**
20
+ * Get display name for agent (with emoji)
21
+ */
22
+ export function getAgentDisplayName(agentName) {
23
+ const emoji = getAgentEmoji(agentName);
24
+ const capitalizedName = agentName.charAt(0).toUpperCase() + agentName.slice(1);
25
+ return `${emoji} ${capitalizedName} Mode`;
26
+ }
@@ -0,0 +1,47 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createBot } from "../bot/index.js";
3
+ import { config } from "../config.js";
4
+ import { reconcileStoredModelSelection } from "../model/manager.js";
5
+ import { loadSettings } from "../settings/manager.js";
6
+ import { processManager } from "../process/manager.js";
7
+ import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
8
+ import { getRuntimeMode } from "../runtime/mode.js";
9
+ import { getRuntimePaths } from "../runtime/paths.js";
10
+ import { logger } from "../utils/logger.js";
11
+ async function getBotVersion() {
12
+ try {
13
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
14
+ const packageJsonContent = await readFile(packageJsonPath, "utf-8");
15
+ const packageJson = JSON.parse(packageJsonContent);
16
+ return packageJson.version ?? "unknown";
17
+ }
18
+ catch (error) {
19
+ logger.warn("[App] Failed to read bot version", error);
20
+ return "unknown";
21
+ }
22
+ }
23
+ export async function startBotApp() {
24
+ const mode = getRuntimeMode();
25
+ const runtimePaths = getRuntimePaths();
26
+ const version = await getBotVersion();
27
+ logger.info(`Starting OpenCode Telegram Group Topics Bot v${version}...`);
28
+ logger.info(`Config loaded from ${runtimePaths.envFilePath}`);
29
+ logger.info(`Allowed User ID: ${config.telegram.allowedUserId}`);
30
+ logger.debug(`[Runtime] Application start mode: ${mode}`);
31
+ await loadSettings();
32
+ await processManager.initialize();
33
+ await reconcileStoredModelSelection();
34
+ await warmupSessionDirectoryCache();
35
+ const bot = createBot();
36
+ const webhookInfo = await bot.api.getWebhookInfo();
37
+ if (webhookInfo.url) {
38
+ logger.info(`[Bot] Webhook detected: ${webhookInfo.url}, removing...`);
39
+ await bot.api.deleteWebhook();
40
+ logger.info("[Bot] Webhook removed, switching to long polling");
41
+ }
42
+ await bot.start({
43
+ onStart: (botInfo) => {
44
+ logger.info(`Bot @${botInfo.username} started!`);
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,116 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { clearAllInteractionState } from "../../interaction/cleanup.js";
3
+ import { INTERACTION_CLEAR_REASON } from "../../interaction/constants.js";
4
+ import { getCurrentSession } from "../../session/manager.js";
5
+ import { TOPIC_SESSION_STATUS } from "../../settings/manager.js";
6
+ import { summaryAggregator } from "../../summary/aggregator.js";
7
+ import { updateTopicBindingStatusBySessionId } from "../../topic/manager.js";
8
+ import { t } from "../../i18n/index.js";
9
+ import { logger } from "../../utils/logger.js";
10
+ import { getScopeKeyFromContext } from "../scope.js";
11
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ function stopLocalStreaming(scopeKey, sessionId) {
13
+ clearAllInteractionState(INTERACTION_CLEAR_REASON.STOP_COMMAND, scopeKey);
14
+ if (!sessionId) {
15
+ return;
16
+ }
17
+ summaryAggregator.clearSession(sessionId);
18
+ updateTopicBindingStatusBySessionId(sessionId, TOPIC_SESSION_STATUS.ABANDONED);
19
+ }
20
+ async function pollSessionStatus(sessionId, directory, maxWaitMs = 5000) {
21
+ const startedAt = Date.now();
22
+ const pollIntervalMs = 500;
23
+ while (Date.now() - startedAt < maxWaitMs) {
24
+ try {
25
+ const { data, error } = await opencodeClient.session.status({ directory });
26
+ if (error || !data) {
27
+ break;
28
+ }
29
+ const sessionStatus = data[sessionId];
30
+ if (!sessionStatus) {
31
+ return "not-found";
32
+ }
33
+ if (sessionStatus.type === "idle" || sessionStatus.type === "error") {
34
+ return "idle";
35
+ }
36
+ if (sessionStatus.type !== "busy") {
37
+ return "not-found";
38
+ }
39
+ await sleep(pollIntervalMs);
40
+ }
41
+ catch (error) {
42
+ logger.warn("[Abort] Failed to poll session status:", error);
43
+ break;
44
+ }
45
+ }
46
+ return "busy";
47
+ }
48
+ export async function abortCurrentOperation(ctx, options = {}) {
49
+ const scopeKey = getScopeKeyFromContext(ctx);
50
+ const currentSession = getCurrentSession(scopeKey);
51
+ if (!currentSession) {
52
+ stopLocalStreaming(scopeKey);
53
+ if (options.notifyUser !== false) {
54
+ await ctx.reply(t("stop.no_active_session"));
55
+ }
56
+ return;
57
+ }
58
+ stopLocalStreaming(scopeKey, currentSession.id);
59
+ const waitingMessage = options.notifyUser === false ? null : await ctx.reply(t("stop.in_progress"));
60
+ const controller = new AbortController();
61
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
62
+ try {
63
+ const { data: abortResult, error: abortError } = await opencodeClient.session.abort({
64
+ sessionID: currentSession.id,
65
+ directory: currentSession.directory,
66
+ }, { signal: controller.signal });
67
+ clearTimeout(timeoutId);
68
+ if (options.notifyUser === false) {
69
+ if (abortError) {
70
+ logger.warn("[Abort] Abort request failed during silent abort:", abortError);
71
+ }
72
+ return;
73
+ }
74
+ if (abortError) {
75
+ logger.warn("[Abort] Abort request failed:", abortError);
76
+ await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_unconfirmed"));
77
+ return;
78
+ }
79
+ if (abortResult !== true) {
80
+ await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_maybe_finished"));
81
+ return;
82
+ }
83
+ const finalStatus = await pollSessionStatus(currentSession.id, currentSession.directory, 5000);
84
+ if (finalStatus === "idle" || finalStatus === "not-found") {
85
+ await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.success"));
86
+ }
87
+ else {
88
+ await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_still_busy"));
89
+ }
90
+ }
91
+ catch (error) {
92
+ clearTimeout(timeoutId);
93
+ if (options.notifyUser === false) {
94
+ if (!(error instanceof Error && error.name === "AbortError")) {
95
+ logger.error("[Abort] Error while aborting session during silent abort:", error);
96
+ }
97
+ return;
98
+ }
99
+ if (error instanceof Error && error.name === "AbortError") {
100
+ await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_timeout"));
101
+ }
102
+ else {
103
+ logger.error("[Abort] Error while aborting session:", error);
104
+ await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_local_only"));
105
+ }
106
+ }
107
+ }
108
+ export async function abortCommand(ctx) {
109
+ try {
110
+ await abortCurrentOperation(ctx);
111
+ }
112
+ catch (error) {
113
+ logger.error("[Abort] Unexpected error:", error);
114
+ await ctx.reply(t("stop.error"));
115
+ }
116
+ }