zardbot-telegram 1.0.0

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 (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
package/.env.example ADDED
@@ -0,0 +1,116 @@
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
+ # Maximum number of commands shown in /commands (default: 10)
44
+ # COMMANDS_LIST_LIMIT=10
45
+
46
+ # Maximum number of scheduled tasks allowed at once (default: 10)
47
+ # TASK_LIMIT=10
48
+
49
+ # Stream update throttle in milliseconds for assistant/tool message edits (default: 500)
50
+ # Higher value = fewer Telegram edit requests, lower value = more real-time updates
51
+ # RESPONSE_STREAM_THROTTLE_MS=500
52
+
53
+ # Bot locale: supported locale code (default: en)
54
+ # Supported locales: en, de, es, fr, ru, zh
55
+ # BOT_LOCALE=en
56
+
57
+ # Hide thinking indicator messages (default: false)
58
+ # HIDE_THINKING_MESSAGES=false
59
+
60
+ # Hide tool call service messages (default: false)
61
+ # HIDE_TOOL_CALL_MESSAGES=false
62
+
63
+ # Assistant message formatting mode (default: markdown)
64
+ # markdown = convert assistant replies to Telegram MarkdownV2
65
+ # raw = show assistant replies as plain text
66
+ # MESSAGE_FORMAT_MODE=markdown
67
+
68
+ # Code File Settings (optional)
69
+ # Maximum file size in KB to send as document (default: 100)
70
+ # CODE_FILE_MAX_SIZE_KB=100
71
+
72
+ # Speech-to-Text / Voice Recognition (optional)
73
+ # Enable voice message transcription by setting a Whisper-compatible API URL.
74
+ # Works with OpenAI, Groq, whisper.cpp, or any Whisper-compatible endpoint.
75
+ # If STT_API_URL is not set, voice messages will get a "not configured" reply.
76
+ #
77
+ # For local whisper.cpp server:
78
+ # STT_API_URL=http://localhost:8080
79
+ # STT_API_KEY= (leave empty for local servers)
80
+ #
81
+ # For OpenAI:
82
+ # STT_API_URL=https://api.openai.com/v1
83
+ # STT_API_KEY=sk-your-api-key
84
+ # STT_MODEL=whisper-1
85
+ #
86
+ # For Groq:
87
+ # STT_API_URL=https://api.groq.com/openai/v1
88
+ # STT_API_KEY=gsk-your-api-key
89
+ # STT_MODEL=whisper-large-v3-turbo
90
+ # STT_API_URL=
91
+ # STT_API_KEY=
92
+ # STT_MODEL=whisper-large-v3-turbo
93
+ # STT_LANGUAGE=
94
+
95
+ # Text-to-Speech (optional)
96
+ # Enable voice responses by setting up a TTS server.
97
+ # Compatible with pocket-tts-server (recommended) or any OpenAI-compatible TTS API.
98
+ #
99
+ # Setup pocket-tts-server: https://github.com/ai-joe-git/pocket-tts-server
100
+ # 82 celebrity voice clones included, local TTS, zero cloud dependency.
101
+ #
102
+ # For local pocket-tts-server:
103
+ # TTS_API_URL=http://localhost:8000
104
+ # TTS_DEFAULT_VOICE=david-attenborough-original
105
+ #
106
+ # For OpenAI:
107
+ # TTS_API_URL=https://api.openai.com/v1
108
+ # TTS_DEFAULT_VOICE=alloy
109
+ # TTS_MODEL=tts-1
110
+ # (requires TTS_API_KEY via OpenAI API key header)
111
+ #
112
+ # If TTS_API_URL is not set, the bot will only send text responses.
113
+ # TTS_API_URL=
114
+ # TTS_DEFAULT_VOICE=david-attenborough-original
115
+ # TTS_MODEL=tts-1
116
+ # TTS_SPEED=1.0
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,250 @@
1
+ # ZardBot Telegram
2
+
3
+ [![npm version](https://img.shields.io/npm/v/zardbot-telegram)](https://www.npmjs.com/package/zardbot-telegram)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
6
+
7
+ ZardBot Telegram is a Telegram client for [OpenCode](https://opencode.ai) with **Text-to-Speech (TTS)** and **Speech-to-Text (STT)** support.
8
+
9
+ Run AI coding tasks, monitor progress, switch models, and manage sessions from your phone -- with voice input and voice responses!
10
+
11
+ **Voice Features:**
12
+ - 🎤 **Speech-to-Text (STT)** -- Send voice messages, transcribe via whisper.cpp
13
+ - 🔊 **Text-to-Speech (TTS)** -- Agent responses read aloud with celebrity voices
14
+
15
+ **Credits:** Forked from [opencode-telegram-bot](https://github.com/grinev/opencode-telegram-bot) by Ruslan Grinev, enhanced with TTS/STT support.
16
+
17
+ ---
18
+
19
+ ## Features
20
+
21
+ All features from the original OpenCode Telegram Bot, plus:
22
+
23
+ - **Voice input (STT)** -- Send voice/audio messages, transcribe them via Whisper-compatible API
24
+ - **Voice output (TTS)** -- Agent responses spoken aloud with 82+ celebrity voices
25
+ - **Voice selection** -- `/voice` command to choose from available voices
26
+ - **Local TTS** -- Works with [pocket-tts-server](https://github.com/ai-joe-git/pocket-tts-server) for zero-cloud voice
27
+
28
+ ### Core Features (from original)
29
+
30
+ - **Remote coding** -- send prompts to OpenCode from anywhere, receive results with code files
31
+ - **Session management** -- create new sessions or continue existing ones
32
+ - **Live status** -- pinned message with project, model, context usage, subagent activity
33
+ - **Model switching** -- pick models from favorites and recent history
34
+ - **Agent modes** -- switch between Plan and Build modes
35
+ - **Subagent activity** -- watch live subagent progress in chat
36
+ - **Custom Commands** -- run OpenCode custom commands from inline menu
37
+ - **Interactive Q&A** -- answer agent questions and approve permissions via buttons
38
+ - **File attachments** -- send images, PDFs, and text-based files to OpenCode
39
+ - **Scheduled tasks** -- schedule prompts to run later or on recurring intervals
40
+ - **Context control** -- compact context when it gets too large
41
+ - **Security** -- strict user ID whitelist
42
+ - **Localization** -- UI in English, Deutsch, Espanol, Francais, Russkii, Zhongwen
43
+
44
+ ---
45
+
46
+ ## Prerequisites
47
+
48
+ - **Node.js 20+** -- [download](https://nodejs.org)
49
+ - **OpenCode** -- install from [opencode.ai](https://opencode.ai) or [GitHub](https://github.com/sst/opencode)
50
+ - **Telegram Bot** -- create one via [@BotFather](https://t.me/BotFather)
51
+
52
+ ### Optional (for voice features)
53
+
54
+ - **pocket-tts-server** -- Local TTS with 82+ celebrity voices: [GitHub](https://github.com/ai-joe-git/pocket-tts-server)
55
+ - **whisper.cpp** -- Local STT: [GitHub](https://github.com/ggerganov/whisper.cpp)
56
+
57
+ ---
58
+
59
+ ## Quick Start
60
+
61
+ ### 1. Create a Telegram Bot
62
+
63
+ 1. Open [@BotFather](https://t.me/BotFather) and send `/newbot`
64
+ 2. Follow prompts to choose name and username
65
+ 3. Copy the **bot token**
66
+
67
+ Get your **Telegram User ID** from [@userinfobot](https://t.me/userinfobot).
68
+
69
+ ### 2. Start OpenCode Server
70
+
71
+ ```bash
72
+ opencode serve
73
+ ```
74
+
75
+ The bot connects to `http://localhost:4096` by default.
76
+
77
+ ### 3. Install & Run
78
+
79
+ ```bash
80
+ npx zardbot-telegram
81
+ ```
82
+
83
+ An interactive wizard will guide you through configuration.
84
+
85
+ #### Alternative: Global Install
86
+
87
+ ```bash
88
+ npm install -g zardbot-telegram
89
+ zardbot-telegram start
90
+ ```
91
+
92
+ To reconfigure at any time:
93
+
94
+ ```bash
95
+ zardbot-telegram config
96
+ ```
97
+
98
+ The setup wizard will guide you through configuration.
99
+
100
+ ---
101
+
102
+ ## Bot Commands
103
+
104
+ | Command | Description |
105
+ |---------|-------------|
106
+ | `/status` | Server health, project, session, model info |
107
+ | `/new` | Create a new session |
108
+ | `/abort` | Abort current task |
109
+ | `/sessions` | Browse and switch sessions |
110
+ | `/projects` | Switch between projects |
111
+ | `/rename` | Rename current session |
112
+ | `/voice` | Select TTS voice (when TTS configured) |
113
+ | `/commands` | Run custom commands |
114
+ | `/task` | Create scheduled task |
115
+ | `/tasklist` | Manage scheduled tasks |
116
+ | `/opencode_start` | Start OpenCode server remotely |
117
+ | `/opencode_stop` | Stop OpenCode server remotely |
118
+ | `/help` | Show available commands |
119
+
120
+ ---
121
+
122
+ ## Configuration
123
+
124
+ Config stored in `.env`:
125
+
126
+ - **macOS:** `~/Library/Application Support/zardbot-telegram/.env`
127
+ - **Windows:** `%APPDATA%\zardbot-telegram\.env`
128
+ - **Linux:** `~/.config/zardbot-telegram/.env`
129
+
130
+ ### Environment Variables
131
+
132
+ | Variable | Description | Required | Default |
133
+ |----------|-------------|:--------:|---------|
134
+ | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | -- |
135
+ | `TELEGRAM_ALLOWED_USER_ID` | Your Telegram user ID | Yes | -- |
136
+ | `TELEGRAM_PROXY_URL` | Proxy for Telegram API | No | -- |
137
+ | `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
138
+ | `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
139
+ | `OPENCODE_SERVER_PASSWORD` | Server auth password | No | -- |
140
+ | `OPENCODE_MODEL_PROVIDER` | Default model provider | Yes | `opencode` |
141
+ | `OPENCODE_MODEL_ID` | Default model ID | Yes | `big-pickle` |
142
+ | `BOT_LOCALE` | UI language (`en`, `de`, `es`, `fr`, `ru`, `zh`) | No | `en` |
143
+ | `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
144
+
145
+ ### Speech-to-Text (STT)
146
+
147
+ | Variable | Description | Required | Default |
148
+ |----------|-------------|:--------:|---------|
149
+ | `STT_API_URL` | Whisper-compatible API URL | No | -- |
150
+ | `STT_API_KEY` | API key for STT provider | No | -- |
151
+ | `STT_MODEL` | STT model name | No | `whisper-large-v3-turbo` |
152
+ | `STT_LANGUAGE` | Language hint | No | -- |
153
+
154
+ #### STT Providers
155
+
156
+ **Local whisper.cpp:**
157
+ ```env
158
+ STT_API_URL=http://localhost:8080
159
+ STT_API_KEY=
160
+ ```
161
+
162
+ **OpenAI:**
163
+ ```env
164
+ STT_API_URL=https://api.openai.com/v1
165
+ STT_API_KEY=sk-your-api-key
166
+ STT_MODEL=whisper-1
167
+ ```
168
+
169
+ **Groq:**
170
+ ```env
171
+ STT_API_URL=https://api.groq.com/openai/v1
172
+ STT_API_KEY=gsk-your-api-key
173
+ STT_MODEL=whisper-large-v3-turbo
174
+ ```
175
+
176
+ ### Text-to-Speech (TTS)
177
+
178
+ | Variable | Description | Required | Default |
179
+ |----------|-------------|:--------:|---------|
180
+ | `TTS_API_URL` | TTS server URL | No | -- |
181
+ | `TTS_DEFAULT_VOICE` | Default voice ID | No | `david-attenborough-original` |
182
+ | `TTS_MODEL` | TTS model name | No | `tts-1` |
183
+ | `TTS_SPEED` | Speech speed multiplier | No | `1.0` |
184
+
185
+ #### TTS Setup (pocket-tts-server)
186
+
187
+ ```env
188
+ TTS_API_URL=http://localhost:8000
189
+ TTS_DEFAULT_VOICE=david-attenborough-original
190
+ ```
191
+
192
+ Setup [pocket-tts-server](https://github.com/ai-joe-git/pocket-tts-server):
193
+ - 82+ celebrity voice clones
194
+ - Local processing (no cloud)
195
+ - No API costs
196
+
197
+ **Available Voices:**
198
+ - `david-attenborough-original`
199
+ - `morgan-freeman-original`
200
+ - `jarvis-iron-man`
201
+ - `margot-robie`
202
+ - `chris-evans`
203
+ - And many more...
204
+
205
+ Use `/voice` in the bot to select from available voices.
206
+
207
+ ---
208
+
209
+ ## Development
210
+
211
+ ### Running from Source
212
+
213
+ ```bash
214
+ git clone https://github.com/ai-joe-git/zardbot-telegram.git
215
+ cd zardbot-telegram
216
+ npm install
217
+ cp .env.example .env
218
+ # Edit .env with your configuration
219
+ npm run dev
220
+ ```
221
+
222
+ ### Scripts
223
+
224
+ | Script | Description |
225
+ |--------|-------------|
226
+ | `npm run dev` | Build and start |
227
+ | `npm run build` | Compile TypeScript |
228
+ | `npm start` | Run compiled code |
229
+ | `npm run lint` | ESLint check |
230
+ | `npm run format` | Format with Prettier |
231
+ | `npm test` | Run tests |
232
+
233
+ ---
234
+
235
+ ## Security
236
+
237
+ Strict **user ID whitelist**. Only `TELEGRAM_ALLOWED_USER_ID` can interact with the bot.
238
+
239
+ ---
240
+
241
+ ## License
242
+
243
+ [MIT](LICENSE)
244
+
245
+ ---
246
+
247
+ ## Acknowledgments
248
+
249
+ - **Ruslan Grinev** -- Original [opencode-telegram-bot](https://github.com/grinev/opencode-telegram-bot)
250
+ - **OpenCode Team** -- [opencode.ai](https://opencode.ai)
@@ -0,0 +1,88 @@
1
+ import { opencodeClient } from "../opencode/client.js";
2
+ import { getCurrentProject } from "../settings/manager.js";
3
+ import { getCurrentSession } from "../session/manager.js";
4
+ import { getCurrentAgent, setCurrentAgent } from "../settings/manager.js";
5
+ import { logger } from "../utils/logger.js";
6
+ /**
7
+ * Get list of available agents from OpenCode API
8
+ * @returns Array of available agents (filtered by mode and hidden flag)
9
+ */
10
+ export async function getAvailableAgents() {
11
+ try {
12
+ const project = getCurrentProject();
13
+ const { data: agents, error } = await opencodeClient.app.agents(project ? { directory: project.worktree } : undefined);
14
+ if (error) {
15
+ logger.error("[AgentManager] Failed to fetch agents:", error);
16
+ return [];
17
+ }
18
+ if (!agents) {
19
+ return [];
20
+ }
21
+ // Filter out hidden agents and subagents (only show primary and all)
22
+ const filtered = agents.filter((agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"));
23
+ logger.debug(`[AgentManager] Fetched ${filtered.length} available agents`);
24
+ return filtered;
25
+ }
26
+ catch (err) {
27
+ logger.error("[AgentManager] Error fetching agents:", err);
28
+ return [];
29
+ }
30
+ }
31
+ const DEFAULT_AGENT = "build";
32
+ /**
33
+ * Get current agent from last session message or settings.
34
+ * Falls back to "build" if nothing is stored.
35
+ * @returns Current agent name
36
+ */
37
+ export async function fetchCurrentAgent() {
38
+ const storedAgent = getCurrentAgent();
39
+ const session = getCurrentSession();
40
+ const project = getCurrentProject();
41
+ if (!session || !project) {
42
+ // No active session, return stored agent from settings
43
+ return storedAgent ?? DEFAULT_AGENT;
44
+ }
45
+ try {
46
+ const { data: messages, error } = await opencodeClient.session.messages({
47
+ sessionID: session.id,
48
+ directory: project.worktree,
49
+ limit: 1,
50
+ });
51
+ if (error || !messages || messages.length === 0) {
52
+ logger.debug("[AgentManager] No messages found, using stored agent");
53
+ return storedAgent ?? DEFAULT_AGENT;
54
+ }
55
+ const lastAgent = messages[0].info.agent;
56
+ logger.debug(`[AgentManager] Current agent from session: ${lastAgent}`);
57
+ // If user explicitly selected an agent in bot settings, prefer it.
58
+ // Session messages may contain stale agent until next prompt is sent.
59
+ if (storedAgent && lastAgent !== storedAgent) {
60
+ logger.debug(`[AgentManager] Using stored agent "${storedAgent}" instead of session agent "${lastAgent}"`);
61
+ return storedAgent;
62
+ }
63
+ // No stored agent yet: sync from session history
64
+ if (lastAgent && lastAgent !== storedAgent) {
65
+ setCurrentAgent(lastAgent);
66
+ }
67
+ return lastAgent || storedAgent || DEFAULT_AGENT;
68
+ }
69
+ catch (err) {
70
+ logger.error("[AgentManager] Error fetching current agent:", err);
71
+ return storedAgent ?? DEFAULT_AGENT;
72
+ }
73
+ }
74
+ /**
75
+ * Select agent and persist to settings
76
+ * @param agentName Name of the agent to select
77
+ */
78
+ export function selectAgent(agentName) {
79
+ logger.info(`[AgentManager] Selected agent: ${agentName}`);
80
+ setCurrentAgent(agentName);
81
+ }
82
+ /**
83
+ * Get stored agent from settings (synchronous)
84
+ * @returns Current agent name or default "build"
85
+ */
86
+ export function getStoredAgent() {
87
+ return getCurrentAgent() ?? "build";
88
+ }
@@ -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,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createBot } from "../bot/index.js";
3
+ import { config } from "../config.js";
4
+ import { loadSettings } from "../settings/manager.js";
5
+ import { processManager } from "../process/manager.js";
6
+ import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
7
+ import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
8
+ import { reconcileStoredModelSelection } from "../model/manager.js";
9
+ import { getRuntimeMode } from "../runtime/mode.js";
10
+ import { getRuntimePaths } from "../runtime/paths.js";
11
+ import { logger } from "../utils/logger.js";
12
+ async function getBotVersion() {
13
+ try {
14
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
15
+ const packageJsonContent = await readFile(packageJsonPath, "utf-8");
16
+ const packageJson = JSON.parse(packageJsonContent);
17
+ return packageJson.version ?? "unknown";
18
+ }
19
+ catch (error) {
20
+ logger.warn("[App] Failed to read bot version", error);
21
+ return "unknown";
22
+ }
23
+ }
24
+ export async function startBotApp() {
25
+ const mode = getRuntimeMode();
26
+ const runtimePaths = getRuntimePaths();
27
+ const version = await getBotVersion();
28
+ logger.info(`Starting OpenCode Telegram Bot v${version}...`);
29
+ logger.info(`Config loaded from ${runtimePaths.envFilePath}`);
30
+ logger.info(`Allowed User ID: ${config.telegram.allowedUserId}`);
31
+ logger.debug(`[Runtime] Application start mode: ${mode}`);
32
+ await loadSettings();
33
+ await processManager.initialize();
34
+ await reconcileStoredModelSelection();
35
+ await warmupSessionDirectoryCache();
36
+ const bot = createBot();
37
+ await scheduledTaskRuntime.initialize(bot);
38
+ const webhookInfo = await bot.api.getWebhookInfo();
39
+ if (webhookInfo.url) {
40
+ logger.info(`[Bot] Webhook detected: ${webhookInfo.url}, removing...`);
41
+ await bot.api.deleteWebhook();
42
+ logger.info("[Bot] Webhook removed, switching to long polling");
43
+ }
44
+ await bot.start({
45
+ onStart: (botInfo) => {
46
+ logger.info(`Bot @${botInfo.username} started!`);
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,121 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { stopEventListening } from "../../opencode/events.js";
3
+ import { getCurrentSession } from "../../session/manager.js";
4
+ import { clearAllInteractionState } from "../../interaction/cleanup.js";
5
+ import { summaryAggregator } from "../../summary/aggregator.js";
6
+ import { logger } from "../../utils/logger.js";
7
+ import { t } from "../../i18n/index.js";
8
+ import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
9
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+ function abortLocalStreaming() {
11
+ stopEventListening();
12
+ summaryAggregator.clear();
13
+ clearAllInteractionState("abort_command");
14
+ }
15
+ async function pollSessionStatus(sessionId, directory, maxWaitMs = 5000) {
16
+ const startedAt = Date.now();
17
+ const pollIntervalMs = 500;
18
+ while (Date.now() - startedAt < maxWaitMs) {
19
+ try {
20
+ const { data, error } = await opencodeClient.session.status({ directory });
21
+ if (error || !data) {
22
+ break;
23
+ }
24
+ const sessionStatus = data[sessionId];
25
+ if (!sessionStatus) {
26
+ return "not-found";
27
+ }
28
+ if (sessionStatus.type === "idle" || sessionStatus.type === "error") {
29
+ return "idle";
30
+ }
31
+ if (sessionStatus.type !== "busy") {
32
+ return "not-found";
33
+ }
34
+ await sleep(pollIntervalMs);
35
+ }
36
+ catch (error) {
37
+ logger.warn("[Abort] Failed to poll session status:", error);
38
+ break;
39
+ }
40
+ }
41
+ return "busy";
42
+ }
43
+ export async function abortCurrentOperation(ctx, options = {}) {
44
+ const notifyUser = options.notifyUser ?? true;
45
+ try {
46
+ abortLocalStreaming();
47
+ const currentSession = getCurrentSession();
48
+ if (!currentSession) {
49
+ if (notifyUser) {
50
+ await ctx.reply(t("stop.no_active_session"));
51
+ }
52
+ return;
53
+ }
54
+ let waitingMessageId = null;
55
+ let chatId = null;
56
+ if (notifyUser) {
57
+ const waitingMessage = await ctx.reply(t("stop.in_progress"));
58
+ waitingMessageId = waitingMessage.message_id;
59
+ chatId = ctx.chat?.id ?? null;
60
+ if (!chatId) {
61
+ logger.warn("[Abort] Chat context is missing while aborting active session");
62
+ return;
63
+ }
64
+ }
65
+ const controller = new AbortController();
66
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
67
+ try {
68
+ const { data: abortResult, error: abortError } = await opencodeClient.session.abort({
69
+ sessionID: currentSession.id,
70
+ directory: currentSession.directory,
71
+ }, { signal: controller.signal });
72
+ clearTimeout(timeoutId);
73
+ if (abortError) {
74
+ logger.warn("[Abort] Abort request failed:", abortError);
75
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
76
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_unconfirmed"));
77
+ }
78
+ return;
79
+ }
80
+ if (abortResult !== true) {
81
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
82
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_maybe_finished"));
83
+ }
84
+ return;
85
+ }
86
+ const finalStatus = await pollSessionStatus(currentSession.id, currentSession.directory, 5000);
87
+ if (finalStatus === "idle" || finalStatus === "not-found") {
88
+ foregroundSessionState.markIdle(currentSession.id);
89
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
90
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.success"));
91
+ }
92
+ }
93
+ else {
94
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
95
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_still_busy"));
96
+ }
97
+ }
98
+ }
99
+ catch (error) {
100
+ clearTimeout(timeoutId);
101
+ if (error instanceof Error && error.name === "AbortError") {
102
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
103
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_timeout"));
104
+ }
105
+ }
106
+ else {
107
+ logger.error("[Abort] Error while aborting session:", error);
108
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
109
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_local_only"));
110
+ }
111
+ }
112
+ }
113
+ }
114
+ catch (error) {
115
+ logger.error("[Abort] Unexpected error:", error);
116
+ await ctx.reply(t("stop.error"));
117
+ }
118
+ }
119
+ export async function abortCommand(ctx) {
120
+ await abortCurrentOperation(ctx);
121
+ }