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.
- package/.env.example +74 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/agent/manager.js +60 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +47 -0
- package/dist/bot/commands/abort.js +116 -0
- package/dist/bot/commands/commands.js +389 -0
- package/dist/bot/commands/constants.js +20 -0
- package/dist/bot/commands/definitions.js +25 -0
- package/dist/bot/commands/help.js +27 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +247 -0
- package/dist/bot/commands/opencode-start.js +85 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +304 -0
- package/dist/bot/commands/rename.js +173 -0
- package/dist/bot/commands/sessions.js +491 -0
- package/dist/bot/commands/start.js +67 -0
- package/dist/bot/commands/status.js +138 -0
- package/dist/bot/constants.js +49 -0
- package/dist/bot/handlers/agent.js +127 -0
- package/dist/bot/handlers/context.js +125 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +124 -0
- package/dist/bot/handlers/model.js +152 -0
- package/dist/bot/handlers/permission.js +281 -0
- package/dist/bot/handlers/prompt.js +263 -0
- package/dist/bot/handlers/question.js +285 -0
- package/dist/bot/handlers/variant.js +147 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +945 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/scope.js +222 -0
- package/dist/bot/telegram-constants.js +3 -0
- package/dist/bot/telegram-rate-limiter.js +263 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/keyboard.js +85 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
- package/dist/bot/utils/session-error-filter.js +34 -0
- package/dist/bot/utils/topic-link.js +29 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +103 -0
- package/dist/i18n/de.js +330 -0
- package/dist/i18n/en.js +330 -0
- package/dist/i18n/es.js +330 -0
- package/dist/i18n/index.js +102 -0
- package/dist/i18n/ru.js +330 -0
- package/dist/i18n/zh.js +330 -0
- package/dist/index.js +28 -0
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/constants.js +25 -0
- package/dist/interaction/guard.js +100 -0
- package/dist/interaction/manager.js +113 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +115 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/manager.js +257 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +159 -0
- package/dist/opencode/prompt-submit-error.js +101 -0
- package/dist/permission/manager.js +92 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +405 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +186 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +64 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/runtime/process-error-handlers.js +24 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +87 -0
- package/dist/settings/manager.js +283 -0
- package/dist/stt/client.js +64 -0
- package/dist/summary/aggregator.js +625 -0
- package/dist/summary/formatter.js +417 -0
- package/dist/summary/tool-message-batcher.js +277 -0
- package/dist/topic/colors.js +8 -0
- package/dist/topic/constants.js +10 -0
- package/dist/topic/manager.js +161 -0
- package/dist/topic/title-constants.js +2 -0
- package/dist/topic/title-format.js +10 -0
- package/dist/topic/title-sync.js +17 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +175 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- 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)
|
|
4
|
+
[](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
|
+
}
|