@grinev/opencode-telegram-bot 0.1.0-rc.4 → 0.1.0-rc.8

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
@@ -28,6 +28,8 @@ OPENCODE_MODEL_ID=big-pickle
28
28
  # Bot Configuration (optional)
29
29
  # Maximum number of sessions shown in /sessions (default: 10)
30
30
  # SESSIONS_LIST_LIMIT=10
31
+ # Bot locale: en or ru (default: en)
32
+ # BOT_LOCALE=en
31
33
 
32
34
  # Code File Settings (optional)
33
35
  # Maximum file size in KB to send as document (default: 100)
@@ -1,12 +1,26 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { createBot } from "../bot/index.js";
2
3
  import { config } from "../config.js";
3
4
  import { loadSettings } from "../settings/manager.js";
4
5
  import { processManager } from "../process/manager.js";
5
6
  import { getRuntimeMode } from "../runtime/mode.js";
6
7
  import { logger } from "../utils/logger.js";
8
+ async function getBotVersion() {
9
+ try {
10
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
11
+ const packageJsonContent = await readFile(packageJsonPath, "utf-8");
12
+ const packageJson = JSON.parse(packageJsonContent);
13
+ return packageJson.version ?? "unknown";
14
+ }
15
+ catch (error) {
16
+ logger.warn("[App] Failed to read bot version", error);
17
+ return "unknown";
18
+ }
19
+ }
7
20
  export async function startBotApp() {
8
21
  const mode = getRuntimeMode();
9
- logger.info("Starting OpenCode Telegram Client...");
22
+ const version = await getBotVersion();
23
+ logger.info(`Starting OpenCode Telegram Bot v${version}...`);
10
24
  logger.info(`Allowed User ID: ${config.telegram.allowedUserId}`);
11
25
  logger.debug(`[Runtime] Application start mode: ${mode}`);
12
26
  await loadSettings();
@@ -1,5 +1,6 @@
1
1
  import { showAgentSelectionMenu } from "../handlers/agent.js";
2
2
  import { logger } from "../../utils/logger.js";
3
+ import { t } from "../../i18n/index.js";
3
4
  /**
4
5
  * Handler for /agent command
5
6
  * Shows inline menu to select agent mode
@@ -11,6 +12,6 @@ export async function handleAgentCommand(ctx) {
11
12
  }
12
13
  catch (err) {
13
14
  logger.error("[AgentCommand] Error showing agent menu:", err);
14
- await ctx.reply("❌ Ошибка при загрузке списка агентов");
15
+ await ctx.reply(t("error.load_agents"));
15
16
  }
16
17
  }
@@ -1,20 +1,21 @@
1
- /**
2
- * Centralized bot commands definitions
3
- * Used for both Telegram API setMyCommands and command handler registration
4
- */
1
+ import { t } from "../../i18n/index.js";
5
2
  /**
6
3
  * List of all bot commands
7
4
  * Update this array when adding new commands
8
5
  */
9
- export const BOT_COMMANDS = [
10
- { command: "status", description: "Статус сервера и сессии" },
11
- { command: "new", description: "Создать новую сессию" },
12
- { command: "stop", description: "Прервать текущее действие" },
13
- { command: "sessions", description: "Список сессий" },
14
- { command: "projects", description: "Список проектов" },
15
- { command: "model", description: "Выбрать модель" },
16
- { command: "agent", description: "Выбрать режим работы" },
17
- { command: "opencode_start", description: "Запустить OpenCode сервер" },
18
- { command: "opencode_stop", description: "Остановить OpenCode сервер" },
19
- { command: "help", description: "Справка" },
6
+ const COMMAND_DEFINITIONS = [
7
+ { command: "status", descriptionKey: "cmd.description.status" },
8
+ { command: "new", descriptionKey: "cmd.description.new" },
9
+ { command: "stop", descriptionKey: "cmd.description.stop" },
10
+ { command: "sessions", descriptionKey: "cmd.description.sessions" },
11
+ { command: "projects", descriptionKey: "cmd.description.projects" },
12
+ { command: "model", descriptionKey: "cmd.description.model" },
13
+ { command: "agent", descriptionKey: "cmd.description.agent" },
14
+ { command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
15
+ { command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
16
+ { command: "help", descriptionKey: "cmd.description.help" },
20
17
  ];
18
+ export const BOT_COMMANDS = COMMAND_DEFINITIONS.map(({ command, descriptionKey }) => ({
19
+ command,
20
+ description: t(descriptionKey),
21
+ }));
@@ -1,7 +1,4 @@
1
+ import { t } from "../../i18n/index.js";
1
2
  export async function helpCommand(ctx) {
2
- await ctx.reply("📖 **Справка**\n\n" +
3
- "/status - Проверить статус сервера\n" +
4
- "/sessions - Список сессий\n" +
5
- "/new - Создать новую сессию\n" +
6
- "/help - Справка", { parse_mode: "Markdown" });
3
+ await ctx.reply(t("help.text"), { parse_mode: "Markdown" });
7
4
  }
@@ -1,5 +1,6 @@
1
1
  import { showModelSelectionMenu } from "../handlers/model.js";
2
2
  import { logger } from "../../utils/logger.js";
3
+ import { t } from "../../i18n/index.js";
3
4
  /**
4
5
  * Handler for /model command
5
6
  * Shows inline menu to select model from favorites
@@ -11,6 +12,6 @@ export async function handleModelCommand(ctx) {
11
12
  }
12
13
  catch (err) {
13
14
  logger.error("[ModelCommand] Error showing model menu:", err);
14
- await ctx.reply("❌ Ошибка при загрузке списка моделей");
15
+ await ctx.reply(t("error.load_models"));
15
16
  }
16
17
  }
@@ -1,23 +1,24 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
2
  import { logger } from "../../utils/logger.js";
3
+ import { t } from "../../i18n/index.js";
3
4
  export async function modelsCommand(ctx) {
4
5
  try {
5
6
  const { data: providersData, error } = await opencodeClient.config.providers();
6
7
  if (error || !providersData) {
7
- await ctx.reply("🔴 Не удалось получить список моделей. Проверьте статус сервера /status.");
8
+ await ctx.reply(t("legacy.models.fetch_error"));
8
9
  return;
9
10
  }
10
11
  const providers = providersData.providers;
11
12
  if (!providers || providers.length === 0) {
12
- await ctx.reply("📋 Нет доступных моделей. Настройте провайдеры через OpenCode.");
13
+ await ctx.reply(t("legacy.models.empty"));
13
14
  return;
14
15
  }
15
- let message = "📋 **Доступные модели:**\n\n";
16
+ let message = t("legacy.models.header");
16
17
  for (const provider of providers) {
17
18
  message += `🔹 **\`${provider.id}\`**\n`;
18
19
  const models = Object.values(provider.models);
19
20
  if (models.length === 0) {
20
- message += " ⚠️ Нет доступных моделей\n";
21
+ message += t("legacy.models.no_provider_models");
21
22
  }
22
23
  else {
23
24
  for (const model of models) {
@@ -26,12 +27,12 @@ export async function modelsCommand(ctx) {
26
27
  }
27
28
  message += "\n";
28
29
  }
29
- message += "💡 Для использования модели в .env:\n";
30
+ message += t("legacy.models.env_hint");
30
31
  message += "```\nOPENCODE_MODEL_PROVIDER=<provider.id>\nOPENCODE_MODEL_ID=<model.id>\n```";
31
32
  await ctx.reply(message, { parse_mode: "Markdown" });
32
33
  }
33
34
  catch (error) {
34
35
  logger.error("[ModelsCommand] Error listing models:", error);
35
- await ctx.reply("🔴 Произошла ошибка при получении списка моделей.");
36
+ await ctx.reply(t("legacy.models.error"));
36
37
  }
37
38
  }
@@ -8,11 +8,12 @@ import { getStoredModel } from "../../model/manager.js";
8
8
  import { formatVariantForButton } from "../../variant/manager.js";
9
9
  import { createMainKeyboard } from "../utils/keyboard.js";
10
10
  import { logger } from "../../utils/logger.js";
11
+ import { t } from "../../i18n/index.js";
11
12
  export async function newCommand(ctx) {
12
13
  try {
13
14
  const currentProject = getCurrentProject();
14
15
  if (!currentProject) {
15
- await ctx.reply("🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.");
16
+ await ctx.reply(t("new.project_not_selected"));
16
17
  return;
17
18
  }
18
19
  logger.debug("[Bot] Creating new session for directory:", currentProject.worktree);
@@ -47,12 +48,12 @@ export async function newCommand(ctx) {
47
48
  const contextInfo = pinnedMessageManager.getContextInfo();
48
49
  const variantName = formatVariantForButton(currentModel.variant || "default");
49
50
  const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
50
- await ctx.reply(`✅ Создана новая сессия: ${session.title}`, {
51
+ await ctx.reply(t("new.created", { title: session.title }), {
51
52
  reply_markup: keyboard,
52
53
  });
53
54
  }
54
55
  catch (error) {
55
56
  logger.error("[Bot] Error creating session:", error);
56
- await ctx.reply("🔴 OpenCode Server недоступен или произошла ошибка при создании сессии.");
57
+ await ctx.reply(t("new.create_error"));
57
58
  }
58
59
  }
@@ -1,6 +1,7 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
2
  import { processManager } from "../../process/manager.js";
3
3
  import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
4
5
  /**
5
6
  * Wait for OpenCode server to become ready by polling health endpoint
6
7
  * @param maxWaitMs Maximum time to wait in milliseconds
@@ -33,18 +34,19 @@ export async function opencodeStartCommand(ctx) {
33
34
  if (processManager.isRunning()) {
34
35
  const uptime = processManager.getUptime();
35
36
  const uptimeStr = uptime ? Math.floor(uptime / 1000) : 0;
36
- await ctx.reply(`⚠️ OpenCode Server уже запущен\n\n` +
37
- `PID: ${processManager.getPID()}\n` +
38
- `Uptime: ${uptimeStr} секунд`);
37
+ await ctx.reply(t("opencode_start.already_running_managed", {
38
+ pid: processManager.getPID() ?? "-",
39
+ seconds: uptimeStr,
40
+ }));
39
41
  return;
40
42
  }
41
43
  // 2. Check if server is accessible (external process)
42
44
  try {
43
45
  const { data, error } = await opencodeClient.global.health();
44
46
  if (!error && data?.healthy) {
45
- await ctx.reply(`✅ OpenCode Server уже запущен внешним процессом\n\n` +
46
- `Версия: ${data.version || "неизвестна"}\n\n` +
47
- `Этот сервер не был запущен через бота, поэтому команда /opencode-stop не сможет его остановить.`);
47
+ await ctx.reply(t("opencode_start.already_running_external", {
48
+ version: data.version || t("common.unknown"),
49
+ }));
48
50
  return;
49
51
  }
50
52
  }
@@ -52,36 +54,32 @@ export async function opencodeStartCommand(ctx) {
52
54
  // Server not accessible, continue with start
53
55
  }
54
56
  // 3. Notify user that we're starting the server
55
- const statusMessage = await ctx.reply("🔄 Запускаю OpenCode Server...");
57
+ const statusMessage = await ctx.reply(t("opencode_start.starting"));
56
58
  // 4. Start the process
57
59
  const { success, error } = await processManager.start();
58
60
  if (!success) {
59
- await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, `🔴 Не удалось запустить OpenCode Server\n\n` +
60
- `Ошибка: ${error || "неизвестная ошибка"}\n\n` +
61
- `Проверьте, что OpenCode CLI установлен и доступен в PATH:\n` +
62
- `\`opencode --version\`\n` +
63
- `\`npm install -g @opencode-ai/cli\``, { parse_mode: "Markdown" });
61
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_start.start_error", { error: error || t("common.unknown_error") }), { parse_mode: "Markdown" });
64
62
  return;
65
63
  }
66
64
  // 5. Wait for server to become ready
67
65
  logger.info("[Bot] Waiting for OpenCode server to become ready...");
68
66
  const ready = await waitForServerReady(10000);
69
67
  if (!ready) {
70
- await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, `⚠️ OpenCode Server запущен, но не отвечает\n\n` +
71
- `PID: ${processManager.getPID()}\n\n` +
72
- `Сервер может запускаться. Попробуйте /status через несколько секунд.`);
68
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_start.started_not_ready", {
69
+ pid: processManager.getPID() ?? "-",
70
+ }));
73
71
  return;
74
72
  }
75
73
  // 6. Get server version and send success message
76
74
  const { data: health } = await opencodeClient.global.health();
77
- await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, `✅ OpenCode Server успешно запущен\n\n` +
78
- `PID: ${processManager.getPID()}\n` +
79
- `Версия: ${health?.version || "неизвестна"}`);
75
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_start.success", {
76
+ pid: processManager.getPID() ?? "-",
77
+ version: health?.version || t("common.unknown"),
78
+ }));
80
79
  logger.info(`[Bot] OpenCode server started successfully, PID=${processManager.getPID()}`);
81
80
  }
82
81
  catch (err) {
83
82
  logger.error("[Bot] Error in /opencode-start command:", err);
84
- await ctx.reply("🔴 Произошла ошибка при запуске сервера.\n\n" +
85
- "Проверьте логи приложения для подробностей.");
83
+ await ctx.reply(t("opencode_start.error"));
86
84
  }
87
85
  }
@@ -1,6 +1,7 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
2
  import { processManager } from "../../process/manager.js";
3
3
  import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
4
5
  /**
5
6
  * Command handler for /opencode-stop
6
7
  * Stops the OpenCode server process
@@ -13,34 +14,31 @@ export async function opencodeStopCommand(ctx) {
13
14
  try {
14
15
  const { data, error } = await opencodeClient.global.health();
15
16
  if (!error && data?.healthy) {
16
- await ctx.reply(`⚠️ OpenCode Server запущен внешним процессом\n\n` +
17
- `Этот сервер не был запущен через /opencode-start.\n` +
18
- `Остановите его вручную или используйте /status для проверки состояния.`);
17
+ await ctx.reply(t("opencode_stop.external_running"));
19
18
  return;
20
19
  }
21
20
  }
22
21
  catch {
23
22
  // Server not accessible
24
23
  }
25
- await ctx.reply("⚠️ OpenCode Server не запущен");
24
+ await ctx.reply(t("opencode_stop.not_running"));
26
25
  return;
27
26
  }
28
27
  // 2. Notify user that we're stopping the server
29
28
  const pid = processManager.getPID();
30
- const statusMessage = await ctx.reply(`🛑 Останавливаю OpenCode Server...\n\nPID: ${pid}`);
29
+ const statusMessage = await ctx.reply(t("opencode_stop.stopping", { pid: pid ?? "-" }));
31
30
  // 3. Stop the process
32
31
  const { success, error } = await processManager.stop(5000);
33
32
  if (!success) {
34
- await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, `🔴 Не удалось остановить OpenCode Server\n\n` + `Ошибка: ${error || "неизвестная ошибка"}`);
33
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_stop.stop_error", { error: error || t("common.unknown_error") }));
35
34
  return;
36
35
  }
37
36
  // 4. Success - process has been stopped
38
- await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, `✅ OpenCode Server успешно остановлен`);
37
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_stop.success"));
39
38
  logger.info("[Bot] OpenCode server stopped successfully");
40
39
  }
41
40
  catch (err) {
42
41
  logger.error("[Bot] Error in /opencode-stop command:", err);
43
- await ctx.reply("🔴 Произошла ошибка при остановке сервера.\n\n" +
44
- "Проверьте логи приложения для подробностей.");
42
+ await ctx.reply(t("opencode_stop.error"));
45
43
  }
46
44
  }
@@ -10,6 +10,7 @@ import { getStoredModel } from "../../model/manager.js";
10
10
  import { formatVariantForButton } from "../../variant/manager.js";
11
11
  import { createMainKeyboard } from "../utils/keyboard.js";
12
12
  import { logger } from "../../utils/logger.js";
13
+ import { t } from "../../i18n/index.js";
13
14
  const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
14
15
  function formatProjectButtonLabel(label, isActive) {
15
16
  const prefix = isActive ? "✅ " : "";
@@ -23,7 +24,7 @@ export async function projectsCommand(ctx) {
23
24
  try {
24
25
  const projects = await getProjects();
25
26
  if (projects.length === 0) {
26
- await ctx.reply("📭 Проектов нет.");
27
+ await ctx.reply(t("projects.empty"));
27
28
  return;
28
29
  }
29
30
  const keyboard = new InlineKeyboard();
@@ -39,15 +40,17 @@ export async function projectsCommand(ctx) {
39
40
  });
40
41
  if (currentProject) {
41
42
  const projectName = currentProject.name || currentProject.worktree;
42
- await ctx.reply(`Выберите проект:\n\nТекущий: 🏗 ${projectName}`, { reply_markup: keyboard });
43
+ await ctx.reply(t("projects.select_with_current", { project: projectName }), {
44
+ reply_markup: keyboard,
45
+ });
43
46
  }
44
47
  else {
45
- await ctx.reply("Выберите проект:", { reply_markup: keyboard });
48
+ await ctx.reply(t("projects.select"), { reply_markup: keyboard });
46
49
  }
47
50
  }
48
51
  catch (error) {
49
52
  logger.error("[Bot] Error fetching projects:", error);
50
- await ctx.reply("🔴 OpenCode Server недоступен или произошла ошибка при получении списка проектов.");
53
+ await ctx.reply(t("projects.fetch_error"));
51
54
  }
52
55
  }
53
56
  export async function handleProjectSelect(ctx) {
@@ -90,7 +93,7 @@ export async function handleProjectSelect(ctx) {
90
93
  const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo, variantName);
91
94
  const projectName = selectedProject.name || selectedProject.worktree;
92
95
  await ctx.answerCallbackQuery();
93
- await ctx.reply(`✅ Проект выбран: ${projectName}\n\n📋 Сессия сброшена. Используйте /sessions или /new для работы с этим проектом.`, {
96
+ await ctx.reply(t("projects.selected", { project: projectName }), {
94
97
  reply_markup: keyboard,
95
98
  });
96
99
  await ctx.deleteMessage();
@@ -98,7 +101,7 @@ export async function handleProjectSelect(ctx) {
98
101
  catch (error) {
99
102
  logger.error("[Bot] Error selecting project:", error);
100
103
  await ctx.answerCallbackQuery();
101
- await ctx.reply("🔴 Ошибка при выборе проекта.");
104
+ await ctx.reply(t("projects.select_error"));
102
105
  }
103
106
  return true;
104
107
  }
@@ -7,12 +7,13 @@ import { keyboardManager } from "../../keyboard/manager.js";
7
7
  import { logger } from "../../utils/logger.js";
8
8
  import { safeBackgroundTask } from "../../utils/safe-background-task.js";
9
9
  import { config } from "../../config.js";
10
+ import { getLocale, t } from "../../i18n/index.js";
10
11
  export async function sessionsCommand(ctx) {
11
12
  try {
12
13
  const maxSessions = config.bot.sessionsListLimit;
13
14
  const currentProject = getCurrentProject();
14
15
  if (!currentProject) {
15
- await ctx.reply("🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.");
16
+ await ctx.reply(t("sessions.project_not_selected"));
16
17
  return;
17
18
  }
18
19
  logger.debug(`[Sessions] Fetching sessions for directory: ${currentProject.worktree}`);
@@ -28,22 +29,23 @@ export async function sessionsCommand(ctx) {
28
29
  logger.debug(`[Sessions] Session: ${session.title} | ${session.directory}`);
29
30
  });
30
31
  if (sessions.length === 0) {
31
- await ctx.reply("📭 Сессий нет.\n\nСоздайте новую сессию командой /new.");
32
+ await ctx.reply(t("sessions.empty"));
32
33
  return;
33
34
  }
34
35
  const keyboard = new InlineKeyboard();
36
+ const localeForDate = getLocale() === "ru" ? "ru-RU" : "en-US";
35
37
  sessions.forEach((session, index) => {
36
- const date = new Date(session.time.created).toLocaleDateString("ru-RU");
38
+ const date = new Date(session.time.created).toLocaleDateString(localeForDate);
37
39
  const label = `${index + 1}. ${session.title} (${date})`;
38
40
  keyboard.text(label, `session:${session.id}`).row();
39
41
  });
40
- await ctx.reply("Выберите сессию:", {
42
+ await ctx.reply(t("sessions.select"), {
41
43
  reply_markup: keyboard,
42
44
  });
43
45
  }
44
46
  catch (error) {
45
47
  logger.error("[Sessions] Error fetching sessions:", error);
46
- await ctx.reply("🔴 OpenCode Server недоступен или произошла ошибка при получении списка сессий.");
48
+ await ctx.reply(t("sessions.fetch_error"));
47
49
  }
48
50
  }
49
51
  export async function handleSessionSelect(ctx) {
@@ -56,7 +58,7 @@ export async function handleSessionSelect(ctx) {
56
58
  const currentProject = getCurrentProject();
57
59
  if (!currentProject) {
58
60
  await ctx.answerCallbackQuery();
59
- await ctx.reply("🔴 Проект не выбран. Используйте /projects.");
61
+ await ctx.reply(t("sessions.select_project_first"));
60
62
  return true;
61
63
  }
62
64
  const { data: session, error } = await opencodeClient.session.get({
@@ -77,7 +79,7 @@ export async function handleSessionSelect(ctx) {
77
79
  let loadingMessageId = null;
78
80
  if (ctx.chat) {
79
81
  try {
80
- const loadingMessage = await ctx.api.sendMessage(ctx.chat.id, `⏳ Загружаю контекст и последние сообщения...`);
82
+ const loadingMessage = await ctx.api.sendMessage(ctx.chat.id, t("sessions.loading_context"));
81
83
  loadingMessageId = loadingMessage.message_id;
82
84
  }
83
85
  catch (err) {
@@ -121,7 +123,7 @@ export async function handleSessionSelect(ctx) {
121
123
  // Send session selection confirmation with updated keyboard
122
124
  const keyboard = keyboardManager.getKeyboard();
123
125
  try {
124
- await ctx.api.sendMessage(chatId, `✅ Сессия выбрана: ${session.title}`, {
126
+ await ctx.api.sendMessage(chatId, t("sessions.selected", { title: session.title }), {
125
127
  reply_markup: keyboard,
126
128
  });
127
129
  }
@@ -139,7 +141,7 @@ export async function handleSessionSelect(ctx) {
139
141
  catch (error) {
140
142
  logger.error("[Sessions] Error selecting session:", error);
141
143
  await ctx.answerCallbackQuery();
142
- await ctx.reply("🔴 Ошибка при выборе сессии.");
144
+ await ctx.reply(t("sessions.select_error"));
143
145
  }
144
146
  return true;
145
147
  }
@@ -202,15 +204,15 @@ async function loadSessionPreview(sessionId, directory) {
202
204
  return [];
203
205
  }
204
206
  }
205
- function formatSessionPreview(sessionTitle, items) {
207
+ function formatSessionPreview(_sessionTitle, items) {
206
208
  const lines = [];
207
209
  if (items.length === 0) {
208
- lines.push("Последних сообщений нет.");
210
+ lines.push(t("sessions.preview.empty"));
209
211
  return lines.join("\n");
210
212
  }
211
- lines.push("Последние сообщения:");
213
+ lines.push(t("sessions.preview.title"));
212
214
  items.forEach((item, index) => {
213
- const label = item.role === "user" ? "Вы:" : "Агент:";
215
+ const label = item.role === "user" ? t("sessions.preview.you") : t("sessions.preview.agent");
214
216
  lines.push(`${label} ${item.text}`);
215
217
  if (index < items.length - 1) {
216
218
  lines.push("");
@@ -4,6 +4,7 @@ import { getStoredModel } from "../../model/manager.js";
4
4
  import { formatVariantForButton } from "../../variant/manager.js";
5
5
  import { pinnedMessageManager } from "../../pinned/manager.js";
6
6
  import { keyboardManager } from "../../keyboard/manager.js";
7
+ import { t } from "../../i18n/index.js";
7
8
  export async function startCommand(ctx) {
8
9
  if (ctx.chat) {
9
10
  if (!pinnedMessageManager.isInitialized()) {
@@ -28,13 +29,5 @@ export async function startCommand(ctx) {
28
29
  keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
29
30
  }
30
31
  const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
31
- await ctx.reply("👋 Добро пожаловать в OpenCode Telegram Bot!\n\n" +
32
- "Используйте команды:\n" +
33
- "/projects — выбрать проект\n" +
34
- "/sessions — список сессий\n" +
35
- "/new — новая сессия\n" +
36
- "/agent — сменить режим\n" +
37
- "/model — выбрать модель\n" +
38
- "/status — статус\n" +
39
- "/help — справка", { reply_markup: keyboard });
32
+ await ctx.reply(t("start.welcome"), { reply_markup: keyboard });
40
33
  }
@@ -7,57 +7,61 @@ import { fetchCurrentModel } from "../../model/manager.js";
7
7
  import { formatModelForDisplay } from "../../model/types.js";
8
8
  import { processManager } from "../../process/manager.js";
9
9
  import { logger } from "../../utils/logger.js";
10
+ import { t } from "../../i18n/index.js";
10
11
  export async function statusCommand(ctx) {
11
12
  try {
12
13
  const { data, error } = await opencodeClient.global.health();
13
14
  if (error || !data) {
14
15
  throw error || new Error("No data received from server");
15
16
  }
16
- let message = "🟢 **OpenCode Server запущен**\n\n";
17
- message += `Статус: ${data.healthy ? "Healthy" : "Unhealthy"}\n`;
17
+ let message = `${t("status.header_running")}\n\n`;
18
+ const healthLabel = data.healthy ? t("status.health.healthy") : t("status.health.unhealthy");
19
+ message += `${t("status.line.health", { health: healthLabel })}\n`;
18
20
  if (data.version) {
19
- message += `Версия: ${data.version}\n`;
21
+ message += `${t("status.line.version", { version: data.version })}\n`;
20
22
  }
21
23
  // Add process management information
22
24
  if (processManager.isRunning()) {
23
25
  const uptime = processManager.getUptime();
24
26
  const uptimeStr = uptime ? Math.floor(uptime / 1000) : 0;
25
- message += `Управляется ботом: Да\n`;
26
- message += `PID: ${processManager.getPID()}\n`;
27
- message += `Uptime: ${uptimeStr} сек\n`;
27
+ message += `${t("status.line.managed_yes")}\n`;
28
+ message += `${t("status.line.pid", { pid: processManager.getPID() ?? "-" })}\n`;
29
+ message += `${t("status.line.uptime_sec", { seconds: uptimeStr })}\n`;
28
30
  }
29
31
  else {
30
- message += `Управляется ботом: Нет\n`;
32
+ message += `${t("status.line.managed_no")}\n`;
31
33
  }
32
34
  // Add agent mode information
33
35
  const currentAgent = await fetchCurrentAgent();
34
- const agentDisplay = currentAgent ? getAgentDisplayName(currentAgent) : "не установлен";
35
- message += `Режим: ${agentDisplay}\n`;
36
+ const agentDisplay = currentAgent
37
+ ? getAgentDisplayName(currentAgent)
38
+ : t("status.agent_not_set");
39
+ message += `${t("status.line.mode", { mode: agentDisplay })}\n`;
36
40
  // Add model information
37
41
  const currentModel = fetchCurrentModel();
38
42
  const modelDisplay = formatModelForDisplay(currentModel.providerID, currentModel.modelID);
39
- message += `Модель: ${modelDisplay}\n`;
43
+ message += `${t("status.line.model", { model: modelDisplay })}\n`;
40
44
  const currentProject = getCurrentProject();
41
45
  if (currentProject) {
42
46
  const projectName = currentProject.name || currentProject.worktree;
43
- message += `\n🏗 Проект: ${projectName}\n`;
47
+ message += `\n${t("status.project_selected", { project: projectName })}\n`;
44
48
  }
45
49
  else {
46
- message += "\n🏗 Проект: не выбран\n";
47
- message += "Используйте /projects для выбора проекта";
50
+ message += `\n${t("status.project_not_selected")}\n`;
51
+ message += t("status.project_hint");
48
52
  }
49
53
  const currentSession = getCurrentSession();
50
54
  if (currentSession) {
51
- message += `\n📋 Текущая сессия: ${currentSession.title}\n`;
55
+ message += `\n${t("status.session_selected", { title: currentSession.title })}\n`;
52
56
  }
53
57
  else {
54
- message += "\n📋 Текущая сессия: не выбрана\n";
55
- message += "Используйте /sessions для выбора или /new для создания";
58
+ message += `\n${t("status.session_not_selected")}\n`;
59
+ message += t("status.session_hint");
56
60
  }
57
61
  await ctx.reply(message, { parse_mode: "Markdown" });
58
62
  }
59
63
  catch (error) {
60
64
  logger.error("[Bot] Error checking server status:", error);
61
- await ctx.reply("🔴 OpenCode Server недоступен\n\n" + "Используйте /opencode_start для запуска сервера.");
65
+ await ctx.reply(t("status.server_unavailable"));
62
66
  }
63
67
  }