@grinev/opencode-telegram-bot 0.16.1 → 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 +5 -0
- package/README.md +57 -42
- package/dist/app/start-bot-app.js +0 -2
- 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 +14 -2
- package/dist/config.js +2 -0
- package/dist/git/worktree.js +158 -0
- package/dist/i18n/de.js +39 -1
- package/dist/i18n/en.js +39 -1
- package/dist/i18n/es.js +39 -1
- package/dist/i18n/fr.js +39 -1
- package/dist/i18n/ru.js +39 -1
- package/dist/i18n/zh.js +39 -1
- 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/settings/manager.js +9 -12
- package/package.json +1 -1
- package/dist/process/manager.js +0 -273
- package/dist/process/types.js +0 -1
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
|
|
@@ -97,6 +101,7 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
97
101
|
# STT_API_KEY=
|
|
98
102
|
# STT_MODEL=
|
|
99
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."
|
|
100
105
|
|
|
101
106
|
# Text-to-Speech credentials (optional)
|
|
102
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,13 +62,15 @@ 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
|
|
|
@@ -76,6 +80,8 @@ The fastest way — run directly with `npx`:
|
|
|
76
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.
|
|
@@ -124,19 +130,21 @@ opencode-telegram config
|
|
|
124
130
|
| `/abort` | Abort the current task |
|
|
125
131
|
| `/sessions` | Browse and switch between recent sessions |
|
|
126
132
|
| `/projects` | Switch between OpenCode projects |
|
|
133
|
+
| `/worktree` | Switch between existing git worktrees |
|
|
127
134
|
| `/open` | Add a project by browsing directories |
|
|
128
135
|
| `/tts` | Toggle audio replies |
|
|
129
136
|
| `/rename` | Rename the current session |
|
|
130
137
|
| `/commands` | Browse and run custom commands |
|
|
138
|
+
| `/skills` | Browse and run OpenCode skills |
|
|
131
139
|
| `/task` | Create a scheduled task |
|
|
132
140
|
| `/tasklist` | Browse and delete scheduled tasks |
|
|
133
|
-
| `/opencode_start` | Start the OpenCode server
|
|
134
|
-
| `/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 |
|
|
135
143
|
| `/help` | Show available commands |
|
|
136
144
|
|
|
137
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.
|
|
138
146
|
|
|
139
|
-
|
|
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.
|
|
140
148
|
|
|
141
149
|
## Scheduled Tasks
|
|
142
150
|
|
|
@@ -146,6 +154,8 @@ Scheduled tasks let you prepare prompts in advance and run them automatically la
|
|
|
146
154
|
- Scheduled executions currently always run with the `build` agent
|
|
147
155
|
- Tasks run outside your active chat session, so they do not interrupt or affect the current session flow
|
|
148
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
|
|
149
159
|
- Up to 10 scheduled tasks can exist at once by default; change this with `TASK_LIMIT` in your `.env`
|
|
150
160
|
|
|
151
161
|
## Configuration
|
|
@@ -164,40 +174,42 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
164
174
|
- **Windows:** `%APPDATA%\opencode-telegram-bot\.env`
|
|
165
175
|
- **Linux:** `~/.config/opencode-telegram-bot/.env`
|
|
166
176
|
|
|
167
|
-
| Variable
|
|
168
|
-
|
|
|
169
|
-
| `TELEGRAM_BOT_TOKEN`
|
|
170
|
-
| `TELEGRAM_ALLOWED_USER_ID`
|
|
171
|
-
| `TELEGRAM_PROXY_URL`
|
|
172
|
-
| `OPENCODE_API_URL`
|
|
173
|
-
| `OPENCODE_SERVER_USERNAME`
|
|
174
|
-
| `OPENCODE_SERVER_PASSWORD`
|
|
175
|
-
| `OPENCODE_MODEL_PROVIDER`
|
|
176
|
-
| `OPENCODE_MODEL_ID`
|
|
177
|
-
| `BOT_LOCALE`
|
|
178
|
-
| `SESSIONS_LIST_LIMIT`
|
|
179
|
-
| `PROJECTS_LIST_LIMIT`
|
|
180
|
-
| `OPEN_BROWSER_ROOTS`
|
|
181
|
-
| `COMMANDS_LIST_LIMIT`
|
|
182
|
-
| `TASK_LIMIT`
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
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` |
|
|
201
213
|
|
|
202
214
|
> **Keep your `.env` file private.** It contains your bot token. Never commit it to version control.
|
|
203
215
|
|
|
@@ -212,6 +224,8 @@ If `STT_API_URL` and `STT_API_KEY` are set, the bot will:
|
|
|
212
224
|
3. Show recognized text in chat
|
|
213
225
|
4. Send the recognized text to OpenCode as a normal prompt
|
|
214
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
|
+
|
|
215
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.
|
|
216
230
|
|
|
217
231
|
TTS configuration example:
|
|
@@ -296,8 +310,9 @@ npm run dev
|
|
|
296
310
|
|
|
297
311
|
**"OpenCode server is not available"**
|
|
298
312
|
|
|
299
|
-
- Ensure
|
|
300
|
-
-
|
|
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
|
|
301
316
|
|
|
302
317
|
**No models in model picker**
|
|
303
318
|
|
|
@@ -3,7 +3,6 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { cleanupBotRuntime, createBot } from "../bot/index.js";
|
|
4
4
|
import { config } from "../config.js";
|
|
5
5
|
import { loadSettings } from "../settings/manager.js";
|
|
6
|
-
import { processManager } from "../process/manager.js";
|
|
7
6
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
8
7
|
import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
|
|
9
8
|
import { reconcileStoredModelSelection } from "../model/manager.js";
|
|
@@ -39,7 +38,6 @@ export async function startBotApp() {
|
|
|
39
38
|
logger.info(`Allowed User ID: ${config.telegram.allowedUserId}`);
|
|
40
39
|
logger.debug(`[Runtime] Application start mode: ${mode}`);
|
|
41
40
|
await loadSettings();
|
|
42
|
-
await processManager.initialize();
|
|
43
41
|
await reconcileStoredModelSelection();
|
|
44
42
|
await warmupSessionDirectoryCache();
|
|
45
43
|
const bot = createBot();
|
|
@@ -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);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
2
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
3
|
+
import { getGitWorktreeContext } from "../../git/worktree.js";
|
|
3
4
|
import { getProjects } from "../../project/manager.js";
|
|
4
5
|
import { syncSessionDirectoryCache } from "../../session/cache-manager.js";
|
|
5
6
|
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
@@ -68,14 +69,37 @@ function buildProjectsMenuText(currentProjectName, page, totalPages) {
|
|
|
68
69
|
total: String(totalPages),
|
|
69
70
|
})}`;
|
|
70
71
|
}
|
|
71
|
-
function
|
|
72
|
+
function worktreeKey(worktree) {
|
|
73
|
+
return process.platform === "win32" ? worktree.toLowerCase() : worktree;
|
|
74
|
+
}
|
|
75
|
+
async function getActiveProjectWorktree() {
|
|
76
|
+
const currentProject = getCurrentProject();
|
|
77
|
+
if (!currentProject) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const worktreeContext = await getGitWorktreeContext(currentProject.worktree);
|
|
82
|
+
if (worktreeContext) {
|
|
83
|
+
return worktreeContext.mainProjectPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.debug("[Projects] Could not resolve active git worktree metadata:", error);
|
|
88
|
+
}
|
|
89
|
+
return currentProject.worktree;
|
|
90
|
+
}
|
|
91
|
+
async function buildProjectsKeyboard(projects, page) {
|
|
72
92
|
const keyboard = new InlineKeyboard();
|
|
73
93
|
const currentProject = getCurrentProject();
|
|
94
|
+
const activeProjectWorktree = await getActiveProjectWorktree();
|
|
74
95
|
const pageSize = config.bot.projectsListLimit;
|
|
75
96
|
const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateProjectsPaginationRange(projects.length, page, pageSize);
|
|
76
97
|
projects.slice(startIndex, endIndex).forEach((project, index) => {
|
|
77
98
|
const isActive = currentProject &&
|
|
78
|
-
(project.id === currentProject.id ||
|
|
99
|
+
(project.id === currentProject.id ||
|
|
100
|
+
project.worktree === currentProject.worktree ||
|
|
101
|
+
(activeProjectWorktree !== null &&
|
|
102
|
+
worktreeKey(project.worktree) === worktreeKey(activeProjectWorktree)));
|
|
79
103
|
const label = buildProjectButtonLabel(startIndex + index, project.worktree);
|
|
80
104
|
const labelWithCheck = formatProjectButtonLabel(label, Boolean(isActive));
|
|
81
105
|
keyboard.text(labelWithCheck, `project:${project.id}`).row();
|
|
@@ -90,14 +114,14 @@ function buildProjectsKeyboard(projects, page) {
|
|
|
90
114
|
}
|
|
91
115
|
return keyboard;
|
|
92
116
|
}
|
|
93
|
-
function buildProjectsMenuView(projects, page) {
|
|
117
|
+
async function buildProjectsMenuView(projects, page) {
|
|
94
118
|
const currentProject = getCurrentProject();
|
|
95
119
|
const pageSize = config.bot.projectsListLimit;
|
|
96
120
|
const { page: normalizedPage, totalPages } = calculateProjectsPaginationRange(projects.length, page, pageSize);
|
|
97
121
|
const currentProjectName = currentProject?.name || currentProject?.worktree || null;
|
|
98
122
|
return {
|
|
99
123
|
text: buildProjectsMenuText(currentProjectName, normalizedPage, totalPages),
|
|
100
|
-
keyboard: buildProjectsKeyboard(projects, normalizedPage),
|
|
124
|
+
keyboard: await buildProjectsKeyboard(projects, normalizedPage),
|
|
101
125
|
};
|
|
102
126
|
}
|
|
103
127
|
export async function projectsCommand(ctx) {
|
|
@@ -112,7 +136,7 @@ export async function projectsCommand(ctx) {
|
|
|
112
136
|
await ctx.reply(t("projects.empty"));
|
|
113
137
|
return;
|
|
114
138
|
}
|
|
115
|
-
const { text, keyboard } = buildProjectsMenuView(projects, 0);
|
|
139
|
+
const { text, keyboard } = await buildProjectsMenuView(projects, 0);
|
|
116
140
|
await replyWithInlineMenu(ctx, {
|
|
117
141
|
menuKind: "project",
|
|
118
142
|
text,
|
|
@@ -146,7 +170,7 @@ export async function handleProjectSelect(ctx) {
|
|
|
146
170
|
await ctx.reply(t("projects.empty"));
|
|
147
171
|
return true;
|
|
148
172
|
}
|
|
149
|
-
const { text, keyboard } = buildProjectsMenuView(projects, page);
|
|
173
|
+
const { text, keyboard } = await buildProjectsMenuView(projects, page);
|
|
150
174
|
await ctx.answerCallbackQuery();
|
|
151
175
|
await ctx.editMessageText(text, {
|
|
152
176
|
reply_markup: appendInlineMenuCancelButton(keyboard, "project"),
|