@grinev/opencode-telegram-bot 0.16.0 → 0.17.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 +8 -0
- package/README.md +72 -42
- package/dist/app/start-bot-app.js +76 -8
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/opencode-start.js +27 -24
- package/dist/bot/commands/opencode-stop.js +51 -23
- package/dist/bot/commands/projects.js +30 -6
- package/dist/bot/commands/skills.js +376 -0
- package/dist/bot/commands/status.js +21 -14
- package/dist/bot/commands/worktree.js +196 -0
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/handlers/voice.js +8 -1
- package/dist/bot/index.js +39 -4
- package/dist/cli/args.js +36 -7
- package/dist/cli.js +131 -17
- package/dist/config.js +3 -0
- package/dist/git/worktree.js +158 -0
- package/dist/i18n/de.js +39 -11
- package/dist/i18n/en.js +39 -11
- package/dist/i18n/es.js +39 -11
- package/dist/i18n/fr.js +39 -11
- package/dist/i18n/ru.js +39 -11
- package/dist/i18n/zh.js +39 -11
- package/dist/opencode/process.js +182 -0
- package/dist/pinned/manager.js +28 -61
- package/dist/project/manager.js +47 -32
- package/dist/runtime/bootstrap.js +119 -7
- package/dist/scheduled-task/executor.js +137 -7
- package/dist/scheduled-task/runtime.js +8 -0
- package/dist/service/manager.js +244 -0
- package/dist/service/runtime.js +19 -0
- package/dist/settings/manager.js +9 -12
- package/package.json +1 -1
- package/dist/process/manager.js +0 -273
- /package/dist/{process → service}/types.js +0 -0
package/.env.example
CHANGED
|
@@ -51,6 +51,10 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
51
51
|
# Maximum number of scheduled tasks allowed at once (default: 10)
|
|
52
52
|
# TASK_LIMIT=10
|
|
53
53
|
|
|
54
|
+
# Maximum execution time for a scheduled task in minutes (default: 120)
|
|
55
|
+
# If exceeded, the bot stops waiting for the result and marks the run as failed.
|
|
56
|
+
# SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES=120
|
|
57
|
+
|
|
54
58
|
# Stream update throttle in milliseconds for assistant/tool message edits (default: 500)
|
|
55
59
|
# Higher value = fewer Telegram edit requests, lower value = more real-time updates
|
|
56
60
|
# RESPONSE_STREAM_THROTTLE_MS=500
|
|
@@ -69,6 +73,9 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
69
73
|
# Hide tool call service messages (default: false)
|
|
70
74
|
# HIDE_TOOL_CALL_MESSAGES=false
|
|
71
75
|
|
|
76
|
+
# Hide tool file edit documents sent as .txt attachments (default: false)
|
|
77
|
+
# HIDE_TOOL_FILE_MESSAGES=false
|
|
78
|
+
|
|
72
79
|
# Assistant message formatting mode (default: markdown)
|
|
73
80
|
# markdown = convert assistant replies to Telegram MarkdownV2
|
|
74
81
|
# raw = show assistant replies as plain text
|
|
@@ -94,6 +101,7 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
94
101
|
# STT_API_KEY=
|
|
95
102
|
# STT_MODEL=
|
|
96
103
|
# STT_LANGUAGE=
|
|
104
|
+
# STT_NOTE_PROMPT="The following text is transcribed from voice. It may contain homophone or phonetic errors. Infer the intended meaning from context."
|
|
97
105
|
|
|
98
106
|
# Text-to-Speech credentials (optional)
|
|
99
107
|
# TTS reply behavior is controlled globally with /tts and persisted in settings.json.
|
package/README.md
CHANGED
|
@@ -26,17 +26,19 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Français (`fr`), Р
|
|
|
26
26
|
|
|
27
27
|
- **Remote coding** — send prompts to OpenCode from anywhere, receive complete results with code sent as files
|
|
28
28
|
- **Session management** — create new sessions or continue existing ones, just like in the TUI
|
|
29
|
-
- **Live status** — pinned message with current project, model, context usage, and changed files list, updated in real time
|
|
29
|
+
- **Live status** — pinned message with current project/worktree, model, context usage, and changed files list, updated in real time
|
|
30
30
|
- **Model switching** — pick models from OpenCode favorites and recent history directly in the chat (favorites are shown first)
|
|
31
31
|
- **Agent modes** — switch between Plan and Build modes on the fly
|
|
32
32
|
- **Subagent activity** — watch live subagent progress in chat, including the current task, agent, model, and active tool step
|
|
33
33
|
- **Custom Commands** — run OpenCode custom commands (and built-ins like `init`/`review`) from an inline menu with confirmation
|
|
34
|
+
- **Skills Catalog** — browse OpenCode skills from an inline menu and run them immediately or with arguments in the next message
|
|
34
35
|
- **Interactive Q&A** — answer agent questions and approve permissions via inline buttons
|
|
35
36
|
- **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, and optionally enable spoken replies with `/tts`
|
|
36
37
|
- **File attachments** — send images, PDF documents, and any text-based files to OpenCode (code, logs, configs etc.)
|
|
37
38
|
- **Scheduled tasks** — schedule prompts to run later or on a recurring interval; see [Scheduled Tasks](#scheduled-tasks)
|
|
38
39
|
- **Context control** — compact context when it gets too large, right from the chat
|
|
39
40
|
- **Input flow control** — when an interactive flow is active, the bot accepts only relevant input to keep context consistent and avoid accidental actions
|
|
41
|
+
- **Git worktree switching** — browse and switch between existing git worktrees for the current repository with `/worktree`
|
|
40
42
|
- **Security** — strict user ID whitelist; no one else can access your bot, even if they find it
|
|
41
43
|
- **Localization** — UI localization is supported for multiple languages (`BOT_LOCALE`)
|
|
42
44
|
|
|
@@ -60,22 +62,26 @@ You'll also need your **Telegram User ID** — send any message to [@userinfobot
|
|
|
60
62
|
|
|
61
63
|
### 2. Start OpenCode Server
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
Run the OpenCode server on the same machine where the bot runs:
|
|
64
66
|
|
|
65
67
|
```bash
|
|
66
68
|
opencode serve
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
> The bot connects to the OpenCode API at `http://localhost:4096` by default.
|
|
71
|
+
> The bot connects to the local OpenCode API at `http://localhost:4096` by default.
|
|
72
|
+
|
|
73
|
+
> After the bot is configured, you can also start and stop the local OpenCode server from Telegram with `/opencode_start` and `/opencode_stop`.
|
|
70
74
|
|
|
71
75
|
### 3. Install & Run
|
|
72
76
|
|
|
73
77
|
The fastest way — run directly with `npx`:
|
|
74
78
|
|
|
75
79
|
```bash
|
|
76
|
-
npx @grinev/opencode-telegram-bot
|
|
80
|
+
npx @grinev/opencode-telegram-bot@latest
|
|
77
81
|
```
|
|
78
82
|
|
|
83
|
+
> **Note:** This README tracks the `main` branch, which may include unreleased changes. The latest npm release may not include every feature described here yet. See [recent commits on `main`](https://github.com/grinev/opencode-telegram-bot/commits/main).
|
|
84
|
+
|
|
79
85
|
> Quick start is for npm usage. You do not need to clone this repository. If you run this command from the source directory (repository root), it may fail with `opencode-telegram: not found`. To run from sources, use the [Development](#development) section.
|
|
80
86
|
|
|
81
87
|
On first launch, an interactive wizard will guide you through the configuration — it asks for interface language first, then your bot token, user ID, OpenCode API URL, and optional OpenCode server credentials (username/password). After that, you're ready to go. Open your bot in Telegram and start sending tasks.
|
|
@@ -87,6 +93,20 @@ npm install -g @grinev/opencode-telegram-bot
|
|
|
87
93
|
opencode-telegram start
|
|
88
94
|
```
|
|
89
95
|
|
|
96
|
+
`start` runs in the foreground by default. This is the recommended mode for `systemd`, Docker, local debugging, and other external process managers.
|
|
97
|
+
|
|
98
|
+
To run the bot in the built-in background mode instead:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
opencode-telegram start --daemon
|
|
102
|
+
opencode-telegram status
|
|
103
|
+
opencode-telegram stop
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> Built-in daemon mode is intended for standalone npm installs without an external supervisor. For `systemd`, `pm2`, or Docker, keep using `opencode-telegram start` without `--daemon`.
|
|
107
|
+
|
|
108
|
+
For Linux `systemd` setup, see [`docs/LINUX_SYSTEMD_SETUP.md`](./docs/LINUX_SYSTEMD_SETUP.md).
|
|
109
|
+
|
|
90
110
|
To reconfigure at any time:
|
|
91
111
|
|
|
92
112
|
```bash
|
|
@@ -110,19 +130,21 @@ opencode-telegram config
|
|
|
110
130
|
| `/abort` | Abort the current task |
|
|
111
131
|
| `/sessions` | Browse and switch between recent sessions |
|
|
112
132
|
| `/projects` | Switch between OpenCode projects |
|
|
133
|
+
| `/worktree` | Switch between existing git worktrees |
|
|
113
134
|
| `/open` | Add a project by browsing directories |
|
|
114
135
|
| `/tts` | Toggle audio replies |
|
|
115
136
|
| `/rename` | Rename the current session |
|
|
116
137
|
| `/commands` | Browse and run custom commands |
|
|
138
|
+
| `/skills` | Browse and run OpenCode skills |
|
|
117
139
|
| `/task` | Create a scheduled task |
|
|
118
140
|
| `/tasklist` | Browse and delete scheduled tasks |
|
|
119
|
-
| `/opencode_start` | Start the OpenCode server
|
|
120
|
-
| `/opencode_stop` | Stop the OpenCode server
|
|
141
|
+
| `/opencode_start` | Start the local OpenCode server on the bot machine |
|
|
142
|
+
| `/opencode_stop` | Stop the local OpenCode server on the bot machine |
|
|
121
143
|
| `/help` | Show available commands |
|
|
122
144
|
|
|
123
145
|
Any regular text message is sent as a prompt to the coding agent only when no blocking interaction is active. Voice/audio messages are transcribed and then sent as prompts when STT is configured.
|
|
124
146
|
|
|
125
|
-
|
|
147
|
+
When the current project is a git repository, `/worktree` shows the existing worktrees for that repository. Status and pinned updates display the main project path with the active branch, and show a separate `Worktree` line when a linked worktree is selected.
|
|
126
148
|
|
|
127
149
|
## Scheduled Tasks
|
|
128
150
|
|
|
@@ -132,6 +154,8 @@ Scheduled tasks let you prepare prompts in advance and run them automatically la
|
|
|
132
154
|
- Scheduled executions currently always run with the `build` agent
|
|
133
155
|
- Tasks run outside your active chat session, so they do not interrupt or affect the current session flow
|
|
134
156
|
- The minimum recurring interval is 5 minutes
|
|
157
|
+
- If a recurring task is still running when its next interval arrives, the bot does not start a parallel copy of the same task and does not replay missed intervals later
|
|
158
|
+
- By default, the bot waits up to 120 minutes for one scheduled task run; change this with `SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES` if needed
|
|
135
159
|
- Up to 10 scheduled tasks can exist at once by default; change this with `TASK_LIMIT` in your `.env`
|
|
136
160
|
|
|
137
161
|
## Configuration
|
|
@@ -150,39 +174,42 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
150
174
|
- **Windows:** `%APPDATA%\opencode-telegram-bot\.env`
|
|
151
175
|
- **Linux:** `~/.config/opencode-telegram-bot/.env`
|
|
152
176
|
|
|
153
|
-
| Variable
|
|
154
|
-
|
|
|
155
|
-
| `TELEGRAM_BOT_TOKEN`
|
|
156
|
-
| `TELEGRAM_ALLOWED_USER_ID`
|
|
157
|
-
| `TELEGRAM_PROXY_URL`
|
|
158
|
-
| `OPENCODE_API_URL`
|
|
159
|
-
| `OPENCODE_SERVER_USERNAME`
|
|
160
|
-
| `OPENCODE_SERVER_PASSWORD`
|
|
161
|
-
| `OPENCODE_MODEL_PROVIDER`
|
|
162
|
-
| `OPENCODE_MODEL_ID`
|
|
163
|
-
| `BOT_LOCALE`
|
|
164
|
-
| `SESSIONS_LIST_LIMIT`
|
|
165
|
-
| `PROJECTS_LIST_LIMIT`
|
|
166
|
-
| `OPEN_BROWSER_ROOTS`
|
|
167
|
-
| `COMMANDS_LIST_LIMIT`
|
|
168
|
-
| `TASK_LIMIT`
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
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 (supported locale code, e.g. `en`, `de`, `es`, `fr`, `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
|
+
| `OPEN_BROWSER_ROOTS` | Comma-separated paths `/open` is allowed to browse (supports `~`) | No | `~` (home directory) |
|
|
191
|
+
| `COMMANDS_LIST_LIMIT` | Items per page in `/commands` and `/skills` | No | `10` |
|
|
192
|
+
| `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
|
|
193
|
+
| `SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES` | Maximum time the bot waits for one scheduled task run before marking it failed | No | `120` |
|
|
194
|
+
| `BASH_TOOL_DISPLAY_MAX_LENGTH` | Maximum displayed length for `bash` tool commands in Telegram summaries; longer commands are truncated | No | `128` |
|
|
195
|
+
| `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval (thinking + tool calls); keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
|
|
196
|
+
| `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
|
|
197
|
+
| `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
|
|
198
|
+
| `HIDE_TOOL_FILE_MESSAGES` | Hide file edit documents sent as `.txt` attachments (`edit_*.txt`, `write_*.txt`) | No | `false` |
|
|
199
|
+
| `RESPONSE_STREAMING` | Stream assistant replies while they are generated across one or more Telegram messages | No | `true` |
|
|
200
|
+
| `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
|
|
201
|
+
| `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
|
|
202
|
+
| `STT_API_URL` | Whisper-compatible API base URL (enables voice/audio transcription) | No | — |
|
|
203
|
+
| `STT_API_KEY` | API key for your STT provider | No | — |
|
|
204
|
+
| `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
|
|
205
|
+
| `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
|
|
206
|
+
| `STT_NOTE_PROMPT` | Optional note prepended to the LLM prompt as `[Note: ...]` for voice transcriptions; empty / `false` / `0` disable it | No | — |
|
|
207
|
+
| `TTS_API_URL` | TTS API base URL | No | — |
|
|
208
|
+
| `TTS_API_KEY` | TTS API key | No | — |
|
|
209
|
+
| `TTS_MODEL` | TTS model name passed to `/audio/speech` | No | `gpt-4o-mini-tts` |
|
|
210
|
+
| `TTS_VOICE` | OpenAI-compatible TTS voice name | No | `alloy` |
|
|
211
|
+
| `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
|
|
212
|
+
| `LOG_RETENTION` | Number of log files to keep: launch files in `sources`, daily files in `installed` | No | `10` |
|
|
186
213
|
|
|
187
214
|
> **Keep your `.env` file private.** It contains your bot token. Never commit it to version control.
|
|
188
215
|
|
|
@@ -197,6 +224,8 @@ If `STT_API_URL` and `STT_API_KEY` are set, the bot will:
|
|
|
197
224
|
3. Show recognized text in chat
|
|
198
225
|
4. Send the recognized text to OpenCode as a normal prompt
|
|
199
226
|
|
|
227
|
+
If `STT_NOTE_PROMPT` is set to a non-empty value other than `false` or `0`, the bot prepends `[Note: ...]` to the transcription before sending it to the LLM. The recognized text shown in Telegram stays unchanged.
|
|
228
|
+
|
|
200
229
|
If TTS credentials are configured, you can toggle spoken replies globally with `/tts`. The preference is stored in `settings.json` and persists across restarts.
|
|
201
230
|
|
|
202
231
|
TTS configuration example:
|
|
@@ -281,8 +310,9 @@ npm run dev
|
|
|
281
310
|
|
|
282
311
|
**"OpenCode server is not available"**
|
|
283
312
|
|
|
284
|
-
- Ensure
|
|
285
|
-
-
|
|
313
|
+
- Ensure an OpenCode server is running at the configured `OPENCODE_API_URL` (default: `http://localhost:4096`)
|
|
314
|
+
- For a local setup, you can start it with `opencode serve` or use `/opencode_start` in Telegram
|
|
315
|
+
- If `OPENCODE_API_URL` points to a remote server, verify that the address is reachable from the bot machine and that the remote server is healthy
|
|
286
316
|
|
|
287
317
|
**No models in model picker**
|
|
288
318
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { createBot } from "../bot/index.js";
|
|
3
|
+
import { cleanupBotRuntime, createBot } from "../bot/index.js";
|
|
3
4
|
import { config } from "../config.js";
|
|
4
5
|
import { loadSettings } from "../settings/manager.js";
|
|
5
|
-
import { processManager } from "../process/manager.js";
|
|
6
6
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
7
7
|
import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
|
|
8
8
|
import { reconcileStoredModelSelection } from "../model/manager.js";
|
|
9
9
|
import { getRuntimeMode } from "../runtime/mode.js";
|
|
10
10
|
import { getRuntimePaths } from "../runtime/paths.js";
|
|
11
|
+
import { clearServiceStateFile } from "../service/manager.js";
|
|
12
|
+
import { getServiceStateFilePathFromEnv, isServiceChildProcess } from "../service/runtime.js";
|
|
11
13
|
import { getLogFilePath, initializeLogger, logger } from "../utils/logger.js";
|
|
14
|
+
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
12
15
|
async function getBotVersion() {
|
|
13
16
|
try {
|
|
14
17
|
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
@@ -35,20 +38,85 @@ export async function startBotApp() {
|
|
|
35
38
|
logger.info(`Allowed User ID: ${config.telegram.allowedUserId}`);
|
|
36
39
|
logger.debug(`[Runtime] Application start mode: ${mode}`);
|
|
37
40
|
await loadSettings();
|
|
38
|
-
await processManager.initialize();
|
|
39
41
|
await reconcileStoredModelSelection();
|
|
40
42
|
await warmupSessionDirectoryCache();
|
|
41
43
|
const bot = createBot();
|
|
42
44
|
await scheduledTaskRuntime.initialize(bot);
|
|
45
|
+
let shutdownStarted = false;
|
|
46
|
+
let serviceStateCleared = false;
|
|
47
|
+
let shutdownTimeout = null;
|
|
48
|
+
const clearManagedServiceState = async () => {
|
|
49
|
+
if (!isServiceChildProcess() || serviceStateCleared) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const stateFilePath = getServiceStateFilePathFromEnv();
|
|
53
|
+
if (!stateFilePath) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await fs.access(stateFilePath);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code === "ENOENT") {
|
|
61
|
+
serviceStateCleared = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
await clearServiceStateFile(stateFilePath);
|
|
67
|
+
serviceStateCleared = true;
|
|
68
|
+
};
|
|
69
|
+
const shutdown = (signal) => {
|
|
70
|
+
if (shutdownStarted) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
shutdownStarted = true;
|
|
74
|
+
logger.info(`[App] Received ${signal}, shutting down...`);
|
|
75
|
+
cleanupBotRuntime(`app_shutdown_${signal.toLowerCase()}`);
|
|
76
|
+
scheduledTaskRuntime.shutdown();
|
|
77
|
+
shutdownTimeout = setTimeout(() => {
|
|
78
|
+
logger.warn(`[App] Shutdown did not finish in ${SHUTDOWN_TIMEOUT_MS}ms, forcing exit.`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
81
|
+
shutdownTimeout.unref?.();
|
|
82
|
+
try {
|
|
83
|
+
bot.stop();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
logger.warn("[App] Failed to stop Telegram bot cleanly", error);
|
|
87
|
+
}
|
|
88
|
+
void clearManagedServiceState().catch((error) => {
|
|
89
|
+
logger.warn("[App] Failed to clear managed service state", error);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const handleSigint = () => shutdown("SIGINT");
|
|
93
|
+
const handleSigterm = () => shutdown("SIGTERM");
|
|
94
|
+
process.on("SIGINT", handleSigint);
|
|
95
|
+
process.on("SIGTERM", handleSigterm);
|
|
43
96
|
const webhookInfo = await bot.api.getWebhookInfo();
|
|
44
97
|
if (webhookInfo.url) {
|
|
45
98
|
logger.info(`[Bot] Webhook detected: ${webhookInfo.url}, removing...`);
|
|
46
99
|
await bot.api.deleteWebhook();
|
|
47
100
|
logger.info("[Bot] Webhook removed, switching to long polling");
|
|
48
101
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
102
|
+
try {
|
|
103
|
+
await bot.start({
|
|
104
|
+
onStart: (botInfo) => {
|
|
105
|
+
logger.info(`Bot @${botInfo.username} started!`);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
process.off("SIGINT", handleSigint);
|
|
111
|
+
process.off("SIGTERM", handleSigterm);
|
|
112
|
+
if (shutdownTimeout) {
|
|
113
|
+
clearTimeout(shutdownTimeout);
|
|
114
|
+
shutdownTimeout = null;
|
|
115
|
+
}
|
|
116
|
+
cleanupBotRuntime("app_shutdown_complete");
|
|
117
|
+
scheduledTaskRuntime.shutdown();
|
|
118
|
+
await clearManagedServiceState().catch((error) => {
|
|
119
|
+
logger.warn("[App] Failed to clear managed service state", error);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
54
122
|
}
|
|
@@ -10,10 +10,12 @@ const COMMAND_DEFINITIONS = [
|
|
|
10
10
|
{ command: "sessions", descriptionKey: "cmd.description.sessions" },
|
|
11
11
|
{ command: "tts", descriptionKey: "cmd.description.tts" },
|
|
12
12
|
{ command: "projects", descriptionKey: "cmd.description.projects" },
|
|
13
|
+
{ command: "worktree", descriptionKey: "cmd.description.worktree" },
|
|
13
14
|
{ command: "task", descriptionKey: "cmd.description.task" },
|
|
14
15
|
{ command: "tasklist", descriptionKey: "cmd.description.tasklist" },
|
|
15
16
|
{ command: "rename", descriptionKey: "cmd.description.rename" },
|
|
16
17
|
{ command: "commands", descriptionKey: "cmd.description.commands" },
|
|
18
|
+
{ command: "skills", descriptionKey: "cmd.description.skills" },
|
|
17
19
|
{ command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
|
|
18
20
|
{ command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
|
|
19
21
|
{ command: "open", descriptionKey: "cmd.description.open" },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { config } from "../../config.js";
|
|
1
3
|
import { opencodeClient } from "../../opencode/client.js";
|
|
2
|
-
import {
|
|
4
|
+
import { resolveLocalOpencodeTarget } from "../../opencode/process.js";
|
|
3
5
|
import { logger } from "../../utils/logger.js";
|
|
4
6
|
import { t } from "../../i18n/index.js";
|
|
5
7
|
import { editBotText } from "../utils/telegram-text.js";
|
|
@@ -31,43 +33,45 @@ async function waitForServerReady(maxWaitMs = 10000) {
|
|
|
31
33
|
*/
|
|
32
34
|
export async function opencodeStartCommand(ctx) {
|
|
33
35
|
try {
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
const uptimeStr = uptime ? Math.floor(uptime / 1000) : 0;
|
|
38
|
-
await ctx.reply(t("opencode_start.already_running_managed", {
|
|
39
|
-
pid: processManager.getPID() ?? "-",
|
|
40
|
-
seconds: uptimeStr,
|
|
41
|
-
}));
|
|
36
|
+
const localTarget = resolveLocalOpencodeTarget(config.opencode.apiUrl);
|
|
37
|
+
if (!localTarget) {
|
|
38
|
+
await ctx.reply(t("opencode_start.remote_configured"));
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
|
-
//
|
|
41
|
+
// Check if server is already accessible.
|
|
45
42
|
try {
|
|
46
43
|
const { data, error } = await opencodeClient.global.health();
|
|
47
44
|
if (!error && data?.healthy) {
|
|
48
|
-
await ctx.reply(t("opencode_start.
|
|
49
|
-
version: data.version || t("common.unknown"),
|
|
50
|
-
}));
|
|
45
|
+
await ctx.reply(t("opencode_start.already_running", { version: data.version || t("common.unknown") }));
|
|
51
46
|
return;
|
|
52
47
|
}
|
|
53
48
|
}
|
|
54
49
|
catch {
|
|
55
|
-
// Server not accessible, continue with start
|
|
50
|
+
// Server not accessible, continue with start.
|
|
56
51
|
}
|
|
57
|
-
// 3. Notify user that we're starting the server
|
|
58
52
|
const statusMessage = await ctx.reply(t("opencode_start.starting"));
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
53
|
+
const isWindows = process.platform === "win32";
|
|
54
|
+
const command = isWindows ? "cmd.exe" : "opencode";
|
|
55
|
+
const args = isWindows ? ["/c", "opencode", "serve"] : ["serve"];
|
|
56
|
+
const childProcess = spawn(command, args, {
|
|
57
|
+
detached: true,
|
|
58
|
+
stdio: "ignore",
|
|
59
|
+
windowsHide: isWindows,
|
|
60
|
+
});
|
|
61
|
+
childProcess.once("error", (error) => {
|
|
62
|
+
logger.error("[Bot] OpenCode server process failed to start", error);
|
|
63
|
+
});
|
|
64
|
+
const pid = childProcess.pid;
|
|
65
|
+
if (!pid) {
|
|
62
66
|
await editBotText({
|
|
63
67
|
api: ctx.api,
|
|
64
68
|
chatId: ctx.chat.id,
|
|
65
69
|
messageId: statusMessage.message_id,
|
|
66
|
-
text: t("opencode_start.start_error", { error:
|
|
70
|
+
text: t("opencode_start.start_error", { error: t("common.unknown_error") }),
|
|
67
71
|
});
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
|
-
|
|
74
|
+
childProcess.unref();
|
|
71
75
|
logger.info("[Bot] Waiting for OpenCode server to become ready...");
|
|
72
76
|
const ready = await waitForServerReady(10000);
|
|
73
77
|
if (!ready) {
|
|
@@ -76,23 +80,22 @@ export async function opencodeStartCommand(ctx) {
|
|
|
76
80
|
chatId: ctx.chat.id,
|
|
77
81
|
messageId: statusMessage.message_id,
|
|
78
82
|
text: t("opencode_start.started_not_ready", {
|
|
79
|
-
pid
|
|
83
|
+
pid,
|
|
80
84
|
}),
|
|
81
85
|
});
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
84
|
-
// 6. Get server version and send success message
|
|
85
88
|
const { data: health } = await opencodeClient.global.health();
|
|
86
89
|
await editBotText({
|
|
87
90
|
api: ctx.api,
|
|
88
91
|
chatId: ctx.chat.id,
|
|
89
92
|
messageId: statusMessage.message_id,
|
|
90
93
|
text: t("opencode_start.success", {
|
|
91
|
-
pid
|
|
94
|
+
pid,
|
|
92
95
|
version: health?.version || t("common.unknown"),
|
|
93
96
|
}),
|
|
94
97
|
});
|
|
95
|
-
logger.info(`[Bot] OpenCode server started successfully, PID=${
|
|
98
|
+
logger.info(`[Bot] OpenCode server started successfully, PID=${pid}, port=${localTarget.port}`);
|
|
96
99
|
}
|
|
97
100
|
catch (err) {
|
|
98
101
|
logger.error("[Bot] Error in /opencode-start command:", err);
|
|
@@ -1,41 +1,69 @@
|
|
|
1
|
+
import { config } from "../../config.js";
|
|
1
2
|
import { opencodeClient } from "../../opencode/client.js";
|
|
2
|
-
import {
|
|
3
|
+
import { findServerPid, killServerProcess, resolveLocalOpencodeTarget, } from "../../opencode/process.js";
|
|
3
4
|
import { logger } from "../../utils/logger.js";
|
|
4
5
|
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { editBotText } from "../utils/telegram-text.js";
|
|
5
7
|
/**
|
|
6
8
|
* Command handler for /opencode-stop
|
|
7
9
|
* Stops the OpenCode server process
|
|
8
10
|
*/
|
|
9
11
|
export async function opencodeStopCommand(ctx) {
|
|
10
12
|
try {
|
|
11
|
-
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
catch {
|
|
22
|
-
// Server not accessible
|
|
13
|
+
const localTarget = resolveLocalOpencodeTarget(config.opencode.apiUrl);
|
|
14
|
+
if (!localTarget) {
|
|
15
|
+
await ctx.reply(t("opencode_stop.remote_configured"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const { data, error } = await opencodeClient.global.health();
|
|
20
|
+
if (error || !data?.healthy) {
|
|
21
|
+
await ctx.reply(t("opencode_stop.not_running"));
|
|
22
|
+
return;
|
|
23
23
|
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
24
26
|
await ctx.reply(t("opencode_stop.not_running"));
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// 3. Stop the process
|
|
31
|
-
const { success, error } = await processManager.stop(5000);
|
|
32
|
-
if (!success) {
|
|
33
|
-
await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_stop.stop_error", { error: error || t("common.unknown_error") }));
|
|
29
|
+
const pid = await findServerPid(localTarget.port);
|
|
30
|
+
if (!pid) {
|
|
31
|
+
await ctx.reply(t("opencode_stop.pid_not_found", { port: localTarget.port }));
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
await
|
|
38
|
-
|
|
34
|
+
const statusMessage = await ctx.reply(t("opencode_stop.stopping", { pid }));
|
|
35
|
+
const stopped = await killServerProcess(pid, 5000);
|
|
36
|
+
if (!stopped) {
|
|
37
|
+
await editBotText({
|
|
38
|
+
api: ctx.api,
|
|
39
|
+
chatId: ctx.chat.id,
|
|
40
|
+
messageId: statusMessage.message_id,
|
|
41
|
+
text: t("opencode_stop.stop_error", { error: t("common.unknown_error") }),
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const { data, error } = await opencodeClient.global.health();
|
|
47
|
+
if (!error && data?.healthy) {
|
|
48
|
+
await editBotText({
|
|
49
|
+
api: ctx.api,
|
|
50
|
+
chatId: ctx.chat.id,
|
|
51
|
+
messageId: statusMessage.message_id,
|
|
52
|
+
text: t("opencode_stop.stop_error", { error: t("opencode_stop.still_running") }),
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Health check failure after stop is expected.
|
|
59
|
+
}
|
|
60
|
+
await editBotText({
|
|
61
|
+
api: ctx.api,
|
|
62
|
+
chatId: ctx.chat.id,
|
|
63
|
+
messageId: statusMessage.message_id,
|
|
64
|
+
text: t("opencode_stop.success"),
|
|
65
|
+
});
|
|
66
|
+
logger.info(`[Bot] OpenCode server stopped successfully, PID=${pid}, port=${localTarget.port}`);
|
|
39
67
|
}
|
|
40
68
|
catch (err) {
|
|
41
69
|
logger.error("[Bot] Error in /opencode-stop command:", err);
|