@grinev/opencode-telegram-bot 0.2.1 → 0.4.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 CHANGED
@@ -4,6 +4,14 @@ TELEGRAM_BOT_TOKEN=
4
4
  # Allowed Telegram User ID (from @userinfobot)
5
5
  TELEGRAM_ALLOWED_USER_ID=
6
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
+
7
15
  # OpenCode API URL (optional, default: http://localhost:4096)
8
16
  # OPENCODE_API_URL=http://localhost:4096
9
17
 
package/README.md CHANGED
@@ -95,6 +95,7 @@ opencode-telegram config
95
95
  | `/projects` | Switch between OpenCode projects |
96
96
  | `/model` | Choose a model from your favorites |
97
97
  | `/agent` | Switch agent mode (Plan / Build) |
98
+ | `/rename` | Rename the current session |
98
99
  | `/opencode_start` | Start the OpenCode server remotely |
99
100
  | `/opencode_stop` | Stop the OpenCode server remotely |
100
101
  | `/help` | Show available commands |
@@ -117,6 +118,7 @@ When installed via npm, the configuration wizard handles the initial setup. The
117
118
  | -------------------------- | -------------------------------------------- | :------: | ----------------------- |
118
119
  | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | — |
119
120
  | `TELEGRAM_ALLOWED_USER_ID` | Your numeric Telegram user ID | Yes | — |
121
+ | `TELEGRAM_PROXY_URL` | Proxy URL for Telegram API (SOCKS5/HTTP) | No | — |
120
122
  | `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
121
123
  | `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
122
124
  | `OPENCODE_SERVER_PASSWORD` | Server auth password | No | — |
@@ -11,6 +11,7 @@ const COMMAND_DEFINITIONS = [
11
11
  { command: "projects", descriptionKey: "cmd.description.projects" },
12
12
  { command: "model", descriptionKey: "cmd.description.model" },
13
13
  { command: "agent", descriptionKey: "cmd.description.agent" },
14
+ { command: "rename", descriptionKey: "cmd.description.rename" },
14
15
  { command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
15
16
  { command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
16
17
  { command: "help", descriptionKey: "cmd.description.help" },
@@ -0,0 +1,91 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { opencodeClient } from "../../opencode/client.js";
3
+ import { getCurrentSession, setCurrentSession } from "../../session/manager.js";
4
+ import { renameManager } from "../../rename/manager.js";
5
+ import { pinnedMessageManager } from "../../pinned/manager.js";
6
+ import { logger } from "../../utils/logger.js";
7
+ import { t } from "../../i18n/index.js";
8
+ export async function renameCommand(ctx) {
9
+ try {
10
+ const currentSession = getCurrentSession();
11
+ if (!currentSession) {
12
+ await ctx.reply(t("rename.no_session"));
13
+ return;
14
+ }
15
+ const keyboard = new InlineKeyboard().text(t("rename.button.cancel"), "rename:cancel");
16
+ const message = await ctx.reply(t("rename.prompt", { title: currentSession.title }), {
17
+ reply_markup: keyboard,
18
+ });
19
+ renameManager.startWaiting(currentSession.id, currentSession.directory, currentSession.title);
20
+ renameManager.setMessageId(message.message_id);
21
+ logger.info(`[RenameCommand] Waiting for new title for session: ${currentSession.id}`);
22
+ }
23
+ catch (error) {
24
+ logger.error("[RenameCommand] Error starting rename flow:", error);
25
+ await ctx.reply(t("rename.error"));
26
+ }
27
+ }
28
+ export async function handleRenameCancel(ctx) {
29
+ const data = ctx.callbackQuery?.data;
30
+ if (!data || data !== "rename:cancel") {
31
+ return false;
32
+ }
33
+ logger.debug("[RenameHandler] Cancel callback received");
34
+ renameManager.clear();
35
+ await ctx.answerCallbackQuery();
36
+ await ctx.editMessageText(t("rename.cancelled"));
37
+ return true;
38
+ }
39
+ export async function handleRenameTextAnswer(ctx) {
40
+ if (!renameManager.isWaitingForName()) {
41
+ return false;
42
+ }
43
+ const text = ctx.message?.text;
44
+ if (!text) {
45
+ return false;
46
+ }
47
+ if (text.startsWith("/")) {
48
+ return false;
49
+ }
50
+ const sessionInfo = renameManager.getSessionInfo();
51
+ if (!sessionInfo) {
52
+ renameManager.clear();
53
+ return false;
54
+ }
55
+ const newTitle = text.trim();
56
+ if (!newTitle) {
57
+ await ctx.reply(t("rename.empty_title"));
58
+ return true;
59
+ }
60
+ logger.info(`[RenameHandler] Renaming session ${sessionInfo.sessionId} to: ${newTitle}`);
61
+ try {
62
+ const { data: updatedSession, error } = await opencodeClient.session.update({
63
+ sessionID: sessionInfo.sessionId,
64
+ directory: sessionInfo.directory,
65
+ title: newTitle,
66
+ });
67
+ if (error || !updatedSession) {
68
+ throw error || new Error("Failed to update session");
69
+ }
70
+ setCurrentSession({
71
+ id: sessionInfo.sessionId,
72
+ title: newTitle,
73
+ directory: sessionInfo.directory,
74
+ });
75
+ if (pinnedMessageManager.isInitialized()) {
76
+ await pinnedMessageManager.onSessionChange(sessionInfo.sessionId, newTitle);
77
+ }
78
+ const messageId = renameManager.getMessageId();
79
+ if (messageId && ctx.chat) {
80
+ await ctx.api.deleteMessage(ctx.chat.id, messageId).catch(() => { });
81
+ }
82
+ await ctx.reply(t("rename.success", { title: newTitle }));
83
+ logger.info(`[RenameHandler] Session renamed successfully: ${newTitle}`);
84
+ }
85
+ catch (error) {
86
+ logger.error("[RenameHandler] Error renaming session:", error);
87
+ await ctx.reply(t("rename.error"));
88
+ }
89
+ renameManager.clear();
90
+ return true;
91
+ }
package/dist/bot/index.js CHANGED
@@ -2,6 +2,8 @@ import { Bot, InputFile } from "grammy";
2
2
  import { promises as fs } from "fs";
3
3
  import * as path from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { SocksProxyAgent } from "socks-proxy-agent";
6
+ import { HttpsProxyAgent } from "https-proxy-agent";
5
7
  import { config } from "../config.js";
6
8
  import { authMiddleware } from "./middleware/auth.js";
7
9
  import { BOT_COMMANDS } from "./commands/definitions.js";
@@ -17,6 +19,7 @@ import { opencodeStartCommand } from "./commands/opencode-start.js";
17
19
  import { opencodeStopCommand } from "./commands/opencode-stop.js";
18
20
  import { handleAgentCommand } from "./commands/agent.js";
19
21
  import { handleModelCommand } from "./commands/model.js";
22
+ import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
20
23
  import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
21
24
  import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
22
25
  import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
@@ -310,7 +313,26 @@ async function resetMismatchedSessionContext() {
310
313
  }
311
314
  }
312
315
  export function createBot() {
313
- const bot = new Bot(config.telegram.token);
316
+ const botOptions = {};
317
+ if (config.telegram.proxyUrl) {
318
+ const proxyUrl = config.telegram.proxyUrl;
319
+ let agent;
320
+ if (proxyUrl.startsWith("socks")) {
321
+ agent = new SocksProxyAgent(proxyUrl);
322
+ logger.info(`[Bot] Using SOCKS proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
323
+ }
324
+ else {
325
+ agent = new HttpsProxyAgent(proxyUrl);
326
+ logger.info(`[Bot] Using HTTP/HTTPS proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
327
+ }
328
+ botOptions.client = {
329
+ baseFetchConfig: {
330
+ agent,
331
+ compress: true,
332
+ },
333
+ };
334
+ }
335
+ const bot = new Bot(config.telegram.token, botOptions);
314
336
  // Heartbeat for diagnostics: verify the event loop is not blocked
315
337
  let heartbeatCounter = 0;
316
338
  setInterval(() => {
@@ -354,6 +376,7 @@ export function createBot() {
354
376
  bot.command("agent", handleAgentCommand);
355
377
  bot.command("model", handleModelCommand);
356
378
  bot.command("stop", stopCommand);
379
+ bot.command("rename", renameCommand);
357
380
  bot.on("callback_query:data", async (ctx) => {
358
381
  logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
359
382
  logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
@@ -367,7 +390,8 @@ export function createBot() {
367
390
  const handledVariant = await handleVariantSelect(ctx);
368
391
  const handledCompactConfirm = await handleCompactConfirm(ctx);
369
392
  const handledCompactCancel = await handleCompactCancel(ctx);
370
- logger.debug(`[Bot] Callback handled: session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compact=${handledCompactConfirm || handledCompactCancel}`);
393
+ const handledRenameCancel = await handleRenameCancel(ctx);
394
+ logger.debug(`[Bot] Callback handled: session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compact=${handledCompactConfirm || handledCompactCancel}, rename=${handledRenameCancel}`);
371
395
  if (!handledSession &&
372
396
  !handledProject &&
373
397
  !handledQuestion &&
@@ -376,7 +400,8 @@ export function createBot() {
376
400
  !handledModel &&
377
401
  !handledVariant &&
378
402
  !handledCompactConfirm &&
379
- !handledCompactCancel) {
403
+ !handledCompactCancel &&
404
+ !handledRenameCancel) {
380
405
  logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
381
406
  await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
382
407
  }
@@ -475,6 +500,10 @@ export function createBot() {
475
500
  await handleQuestionTextAnswer(ctx);
476
501
  return;
477
502
  }
503
+ const handledRename = await handleRenameTextAnswer(ctx);
504
+ if (handledRename) {
505
+ return;
506
+ }
478
507
  const currentProject = getCurrentProject();
479
508
  if (!currentProject) {
480
509
  await ctx.reply(t("bot.project_not_selected"));
package/dist/config.js CHANGED
@@ -38,6 +38,7 @@ export const config = {
38
38
  telegram: {
39
39
  token: getEnvVar("TELEGRAM_BOT_TOKEN"),
40
40
  allowedUserId: parseInt(getEnvVar("TELEGRAM_ALLOWED_USER_ID"), 10),
41
+ proxyUrl: getEnvVar("TELEGRAM_PROXY_URL", false),
41
42
  },
42
43
  opencode: {
43
44
  apiUrl: getEnvVar("OPENCODE_API_URL", false) || "http://localhost:4096",
package/dist/i18n/en.js CHANGED
@@ -187,6 +187,14 @@ export const en = {
187
187
  "runtime.wizard.saved": "Configuration saved:\n- {envPath}\n- {settingsPath}\n",
188
188
  "runtime.wizard.not_configured_starting": "Application is not configured yet. Starting wizard...\n",
189
189
  "runtime.wizard.tty_required": "Interactive wizard requires a TTY terminal. Run `opencode-telegram config` in an interactive shell.",
190
+ "rename.no_session": "⚠️ No active session. Create or select a session first.",
191
+ "rename.prompt": "📝 Enter new title for session:\n\nCurrent: {title}",
192
+ "rename.empty_title": "⚠️ Title cannot be empty.",
193
+ "rename.success": "✅ Session renamed to: {title}",
194
+ "rename.error": "🔴 Failed to rename session.",
195
+ "rename.cancelled": "❌ Rename cancelled.",
196
+ "rename.button.cancel": "❌ Cancel",
197
+ "cmd.description.rename": "Rename current session",
190
198
  "cli.usage": "Usage:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nNotes:\n - No command defaults to `start`\n - `--mode` is currently supported for `start` only",
191
199
  "cli.placeholder.status": "Command `status` is currently a placeholder. Real status checks will be added in service layer (Phase 5).",
192
200
  "cli.placeholder.stop": "Command `stop` is currently a placeholder. Real background process stop will be added in service layer (Phase 5).",
package/dist/i18n/ru.js CHANGED
@@ -187,6 +187,14 @@ export const ru = {
187
187
  "runtime.wizard.saved": "Конфигурация сохранена:\n- {envPath}\n- {settingsPath}\n",
188
188
  "runtime.wizard.not_configured_starting": "Приложение еще не сконфигурировано. Запускаю wizard...\n",
189
189
  "runtime.wizard.tty_required": "Интерактивный wizard требует TTY-терминал. Запустите `opencode-telegram config` в интерактивной оболочке.",
190
+ "rename.no_session": "⚠️ Нет активной сессии. Сначала создайте или выберите сессию.",
191
+ "rename.prompt": "📝 Введите новое название сессии:\n\nТекущее: {title}",
192
+ "rename.empty_title": "⚠️ Название не может быть пустым.",
193
+ "rename.success": "✅ Сессия переименована в: {title}",
194
+ "rename.error": "🔴 Не удалось переименовать сессию.",
195
+ "rename.cancelled": "❌ Переименование отменено.",
196
+ "rename.button.cancel": "❌ Отмена",
197
+ "cmd.description.rename": "Переименовать текущую сессию",
190
198
  "cli.usage": "Использование:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nЗаметки:\n - Без команды по умолчанию используется `start`\n - `--mode` сейчас поддерживается только для `start`",
191
199
  "cli.placeholder.status": "Команда `status` пока работает как заглушка. Реальная проверка статуса появится на этапе service-слоя (Этап 5).",
192
200
  "cli.placeholder.stop": "Команда `stop` пока работает как заглушка. Реальная остановка фонового процесса появится на этапе service-слоя (Этап 5).",
@@ -0,0 +1,50 @@
1
+ import { logger } from "../utils/logger.js";
2
+ class RenameManager {
3
+ state = {
4
+ isWaiting: false,
5
+ sessionId: null,
6
+ sessionDirectory: null,
7
+ currentTitle: null,
8
+ messageId: null,
9
+ };
10
+ startWaiting(sessionId, directory, currentTitle) {
11
+ logger.info(`[RenameManager] Starting rename flow for session: ${sessionId}`);
12
+ this.state = {
13
+ isWaiting: true,
14
+ sessionId,
15
+ sessionDirectory: directory,
16
+ currentTitle,
17
+ messageId: null,
18
+ };
19
+ }
20
+ setMessageId(messageId) {
21
+ this.state.messageId = messageId;
22
+ }
23
+ getMessageId() {
24
+ return this.state.messageId;
25
+ }
26
+ isWaitingForName() {
27
+ return this.state.isWaiting;
28
+ }
29
+ getSessionInfo() {
30
+ if (!this.state.isWaiting || !this.state.sessionId) {
31
+ return null;
32
+ }
33
+ return {
34
+ sessionId: this.state.sessionId,
35
+ directory: this.state.sessionDirectory,
36
+ currentTitle: this.state.currentTitle,
37
+ };
38
+ }
39
+ clear() {
40
+ logger.debug("[RenameManager] Clearing rename state");
41
+ this.state = {
42
+ isWaiting: false,
43
+ sessionId: null,
44
+ sessionDirectory: null,
45
+ currentTitle: null,
46
+ messageId: null,
47
+ };
48
+ }
49
+ }
50
+ export const renameManager = new RenameManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,7 +55,9 @@
55
55
  "@opencode-ai/sdk": "^1.1.21",
56
56
  "better-sqlite3": "^12.6.2",
57
57
  "dotenv": "^17.2.3",
58
- "grammy": "^1.39.2"
58
+ "grammy": "^1.39.2",
59
+ "https-proxy-agent": "^7.0.6",
60
+ "socks-proxy-agent": "^8.0.5"
59
61
  },
60
62
  "devDependencies": {
61
63
  "@types/better-sqlite3": "^7.6.13",