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.
- package/.env.example +116 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/agent/manager.js +88 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +49 -0
- package/dist/bot/commands/abort.js +121 -0
- package/dist/bot/commands/commands.js +480 -0
- package/dist/bot/commands/definitions.js +27 -0
- package/dist/bot/commands/help.js +10 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +70 -0
- package/dist/bot/commands/opencode-start.js +101 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +223 -0
- package/dist/bot/commands/rename.js +139 -0
- package/dist/bot/commands/sessions.js +351 -0
- package/dist/bot/commands/start.js +43 -0
- package/dist/bot/commands/status.js +95 -0
- package/dist/bot/commands/task.js +399 -0
- package/dist/bot/commands/tasklist.js +220 -0
- package/dist/bot/commands/voice.js +145 -0
- package/dist/bot/handlers/agent.js +118 -0
- package/dist/bot/handlers/context.js +100 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +143 -0
- package/dist/bot/handlers/permission.js +235 -0
- package/dist/bot/handlers/prompt.js +240 -0
- package/dist/bot/handlers/question.js +390 -0
- package/dist/bot/handlers/tts.js +89 -0
- package/dist/bot/handlers/variant.js +138 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +977 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +95 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/streaming/response-streamer.js +286 -0
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/finalize-assistant-response.js +52 -0
- package/dist/bot/utils/keyboard.js +69 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
- package/dist/bot/utils/telegram-text.js +28 -0
- package/dist/bot/utils/thinking-message.js +8 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +97 -0
- package/dist/i18n/de.js +357 -0
- package/dist/i18n/en.js +357 -0
- package/dist/i18n/es.js +357 -0
- package/dist/i18n/fr.js +357 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/ru.js +357 -0
- package/dist/i18n/zh.js +357 -0
- package/dist/index.js +26 -0
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/cleanup.js +32 -0
- package/dist/interaction/guard.js +140 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +172 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/context-limit.js +57 -0
- package/dist/model/manager.js +259 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +140 -0
- package/dist/permission/manager.js +100 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +682 -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 +176 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +53 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/scheduled-task/creation-manager.js +113 -0
- package/dist/scheduled-task/display.js +239 -0
- package/dist/scheduled-task/executor.js +87 -0
- package/dist/scheduled-task/foreground-state.js +32 -0
- package/dist/scheduled-task/next-run.js +207 -0
- package/dist/scheduled-task/runtime.js +368 -0
- package/dist/scheduled-task/schedule-parser.js +169 -0
- package/dist/scheduled-task/store.js +65 -0
- package/dist/scheduled-task/types.js +19 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +10 -0
- package/dist/settings/manager.js +158 -0
- package/dist/stt/client.js +97 -0
- package/dist/summary/aggregator.js +1136 -0
- package/dist/summary/formatter.js +491 -0
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +90 -0
- package/dist/tts/client.js +130 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +127 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- 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
|
+
[](https://www.npmjs.com/package/zardbot-telegram)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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
|
+
}
|