@grinev/opencode-telegram-bot 0.14.1 → 0.15.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/dist/i18n/ru.js CHANGED
@@ -3,6 +3,7 @@ export const ru = {
3
3
  "cmd.description.new": "Создать новую сессию",
4
4
  "cmd.description.stop": "Прервать текущее действие",
5
5
  "cmd.description.sessions": "Список сессий",
6
+ "cmd.description.tts": "Переключить аудиоответы",
6
7
  "cmd.description.projects": "Список проектов",
7
8
  "cmd.description.task": "Создать задачу по расписанию",
8
9
  "cmd.description.tasklist": "Список задач по расписанию",
@@ -32,8 +33,8 @@ export const ru = {
32
33
  "inline.cancelled_callback": "Отменено",
33
34
  "common.unknown": "неизвестна",
34
35
  "common.unknown_error": "неизвестная ошибка",
35
- "start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/task — задача по расписанию\n/tasklist — список задач по расписанию\n/status — статус\n/help — справка\n\nРежим, модель и вариант выбираются кнопками внизу.",
36
- "help.keyboard_hint": "💡 Режим, модель, вариант и действия с контекстом доступны через нижние кнопки клавиатуры.",
36
+ "start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/task — задача по расписанию\n/tasklist — список задач по расписанию\n/status — статус\n/help — справка\n\nАгент, модель и вариант выбираются кнопками внизу.",
37
+ "help.keyboard_hint": "💡 Агент, модель, вариант и действия с контекстом доступны через нижние кнопки клавиатуры.",
37
38
  "help.text": "📖 **Справка**\n\n/status - Проверить статус сервера\n/sessions - Список сессий\n/new - Создать новую сессию\n/help - Справка",
38
39
  "bot.thinking": "💭 Думаю...",
39
40
  "bot.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
@@ -65,8 +66,11 @@ export const ru = {
65
66
  "status.line.managed_no": "Запущен ботом: Нет",
66
67
  "status.line.pid": "PID: {pid}",
67
68
  "status.line.uptime_sec": "Uptime: {seconds} сек",
68
- "status.line.mode": "Режим: {mode}",
69
+ "status.line.mode": "Агент: {mode}",
69
70
  "status.line.model": "Модель: {model}",
71
+ "status.line.tts": "TTS-ответы: {tts}",
72
+ "status.tts.on": "Вкл",
73
+ "status.tts.off": "Выкл",
70
74
  "status.agent_not_set": "не установлен",
71
75
  "status.project_selected": "Проект: {project}",
72
76
  "status.project_not_selected": "Проект: не выбран",
@@ -75,6 +79,10 @@ export const ru = {
75
79
  "status.session_not_selected": "Текущая сессия: не выбрана",
76
80
  "status.session_hint": "Используйте /sessions для выбора или /new для создания",
77
81
  "status.server_unavailable": "🔴 OpenCode Server недоступен\n\nИспользуйте /opencode_start для запуска сервера.",
82
+ "tts.enabled": "🔊 Аудиоответы включены глобально.",
83
+ "tts.not_configured": "⚠️ Аудиоответы недоступны. Сначала укажите `TTS_API_URL` и `TTS_API_KEY`.",
84
+ "tts.disabled": "🔇 Аудиоответы выключены глобально.",
85
+ "tts.failed": "⚠️ Не удалось создать аудиоответ.",
78
86
  "projects.empty": "📭 Проектов нет.\n\nОткройте директорию в OpenCode и создайте хотя бы одну сессию, после этого она появится здесь.",
79
87
  "projects.select": "Выберите проект:",
80
88
  "projects.select_with_current": "Выберите проект:\n\nТекущий: 🏗 {project}",
@@ -127,11 +135,11 @@ export const ru = {
127
135
  "opencode_stop.stop_error": "🔴 Не удалось остановить OpenCode Server\n\nОшибка: {error}",
128
136
  "opencode_stop.success": "✅ OpenCode Server успешно остановлен",
129
137
  "opencode_stop.error": "🔴 Произошла ошибка при остановке сервера.\n\nПроверьте логи приложения для подробностей.",
130
- "agent.changed_callback": "Режим изменен: {name}",
131
- "agent.changed_message": "✅ Режим изменен на: {name}",
132
- "agent.change_error_callback": "Ошибка при смене режима",
133
- "agent.menu.current": "Текущий режим: {name}\n\nВыберите режим:",
134
- "agent.menu.select": "Выберите режим работы:",
138
+ "agent.changed_callback": "Агент изменен: {name}",
139
+ "agent.changed_message": "✅ Агент изменен на: {name}",
140
+ "agent.change_error_callback": "Ошибка при смене агента",
141
+ "agent.menu.current": "Текущий агент: {name}\n\nВыберите агента:",
142
+ "agent.menu.select": "Выберите агента:",
135
143
  "agent.menu.empty": "⚠️ Нет доступных агентов",
136
144
  "agent.menu.error": "🔴 Не удалось получить список агентов",
137
145
  "model.changed_callback": "Модель изменена: {name}",
@@ -205,7 +213,7 @@ export const ru = {
205
213
  "question.summary.title": "✅ Опрос завершен!\n\n",
206
214
  "question.summary.question": "Вопрос {index}:\n{question}\n\n",
207
215
  "question.summary.answer": "Ответ:\n{answer}\n\n",
208
- "keyboard.agent_mode": "{emoji} {name} Mode",
216
+ "keyboard.agent_mode": "{emoji} {name} Agent",
209
217
  "keyboard.context": "📊 {used} / {limit} ({percent}%)",
210
218
  "keyboard.context_empty": "📊 0",
211
219
  "keyboard.variant": "💭 {name}",
package/dist/i18n/zh.js CHANGED
@@ -3,6 +3,7 @@ export const zh = {
3
3
  "cmd.description.new": "创建新会话",
4
4
  "cmd.description.stop": "停止当前操作",
5
5
  "cmd.description.sessions": "列出会话",
6
+ "cmd.description.tts": "切换语音回复",
6
7
  "cmd.description.projects": "列出项目",
7
8
  "cmd.description.task": "创建定时任务",
8
9
  "cmd.description.tasklist": "查看定时任务",
@@ -32,8 +33,8 @@ export const zh = {
32
33
  "inline.cancelled_callback": "已取消",
33
34
  "common.unknown": "未知",
34
35
  "common.unknown_error": "未知错误",
35
- "start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/task — 定时任务\n/tasklist — 定时任务列表\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择模式、模型和变体。",
36
- "help.keyboard_hint": "💡 代理模式、模型、变体和上下文操作请使用底部键盘按钮。",
36
+ "start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/task — 定时任务\n/tasklist — 定时任务列表\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择 Agent、模型和变体。",
37
+ "help.keyboard_hint": "💡 Agent、模型、变体和上下文操作请使用底部键盘按钮。",
37
38
  "help.text": "📖 **帮助**\n\n/status - 查看服务器状态\n/sessions - 会话列表\n/new - 创建新会话\n/help - 帮助",
38
39
  "bot.thinking": "💭 思考中...",
39
40
  "bot.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
@@ -65,8 +66,11 @@ export const zh = {
65
66
  "status.line.managed_no": "由机器人启动:否",
66
67
  "status.line.pid": "PID:{pid}",
67
68
  "status.line.uptime_sec": "运行时间:{seconds} 秒",
68
- "status.line.mode": "模式:{mode}",
69
+ "status.line.mode": "Agent:{mode}",
69
70
  "status.line.model": "模型:{model}",
71
+ "status.line.tts": "TTS 回复:{tts}",
72
+ "status.tts.on": "开启",
73
+ "status.tts.off": "关闭",
70
74
  "status.agent_not_set": "未设置",
71
75
  "status.project_selected": "项目:{project}",
72
76
  "status.project_not_selected": "项目:未选择",
@@ -75,6 +79,10 @@ export const zh = {
75
79
  "status.session_not_selected": "当前会话:未选择",
76
80
  "status.session_hint": "使用 /sessions 选择一个会话,或 /new 创建",
77
81
  "status.server_unavailable": "🔴 OpenCode 服务器不可用\n\n使用 /opencode_start 启动服务器。",
82
+ "tts.enabled": "🔊 已全局启用语音回复。",
83
+ "tts.not_configured": "⚠️ 语音回复暂不可用。请先设置 `TTS_API_URL` 和 `TTS_API_KEY`。",
84
+ "tts.disabled": "🔇 已全局关闭语音回复。",
85
+ "tts.failed": "⚠️ 生成语音回复失败。",
78
86
  "projects.empty": "📭 未找到项目。\n\n在 OpenCode 中打开一个目录并至少创建一个会话,然后它会出现在这里。",
79
87
  "projects.select": "请选择一个项目:",
80
88
  "projects.select_with_current": "请选择一个项目:\n\n当前:🏗 {project}",
@@ -127,11 +135,11 @@ export const zh = {
127
135
  "opencode_stop.stop_error": "🔴 停止 OpenCode 服务器失败\n\n错误:{error}",
128
136
  "opencode_stop.success": "✅ OpenCode 服务器已成功停止",
129
137
  "opencode_stop.error": "🔴 停止服务器时发生错误。\n\n请查看应用日志了解详情。",
130
- "agent.changed_callback": "模式已更改:{name}",
131
- "agent.changed_message": "✅ 模式已切换为:{name}",
132
- "agent.change_error_callback": "切换模式失败",
133
- "agent.menu.current": "当前模式:{name}\n\n请选择模式:",
134
- "agent.menu.select": "请选择工作模式:",
138
+ "agent.changed_callback": "Agent 已更改:{name}",
139
+ "agent.changed_message": "✅ Agent 已切换为:{name}",
140
+ "agent.change_error_callback": "切换 Agent 失败",
141
+ "agent.menu.current": "当前 Agent:{name}\n\n请选择 Agent:",
142
+ "agent.menu.select": "请选择 Agent:",
135
143
  "agent.menu.empty": "⚠️ 没有可用的代理",
136
144
  "agent.menu.error": "🔴 获取代理列表失败",
137
145
  "model.changed_callback": "模型已更改:{name}",
@@ -205,7 +213,7 @@ export const zh = {
205
213
  "question.summary.title": "✅ 投票已完成!\n\n",
206
214
  "question.summary.question": "问题 {index}:\n{question}\n\n",
207
215
  "question.summary.answer": "回答:\n{answer}\n\n",
208
- "keyboard.agent_mode": "{emoji} {name} 模式",
216
+ "keyboard.agent_mode": "{emoji} {name} Agent",
209
217
  "keyboard.context": "📊 {used} / {limit} ({percent}%)",
210
218
  "keyboard.context_empty": "📊 0",
211
219
  "keyboard.variant": "💭 {name}",
@@ -1,3 +1,5 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { logger } from "../utils/logger.js";
2
4
  import { opencodeClient } from "../opencode/client.js";
3
5
  import { getCurrentSession } from "../session/manager.js";
@@ -15,6 +17,7 @@ class PinnedMessageManager {
15
17
  sessionId: null,
16
18
  sessionTitle: t("pinned.default_session_title"),
17
19
  projectName: "",
20
+ projectBranch: null,
18
21
  tokensUsed: 0,
19
22
  tokensLimit: 0,
20
23
  lastUpdated: 0,
@@ -52,9 +55,7 @@ class PinnedMessageManager {
52
55
  // Update state
53
56
  this.state.sessionId = sessionId;
54
57
  this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
55
- const project = getCurrentProject();
56
- this.state.projectName =
57
- project?.name || this.extractProjectName(project?.worktree) || t("pinned.unknown");
58
+ await this.refreshProjectMetadata();
58
59
  // Fetch context limit for current model
59
60
  await this.fetchContextLimit();
60
61
  // Trigger keyboard update callback with reset context (0 tokens)
@@ -171,6 +172,7 @@ class PinnedMessageManager {
171
172
  * Used at thinking time to push accumulated silent updates to Telegram.
172
173
  */
173
174
  async refresh() {
175
+ await this.refreshProjectMetadata();
174
176
  await this.updatePinnedMessage(true);
175
177
  }
176
178
  /**
@@ -412,6 +414,61 @@ class PinnedMessageManager {
412
414
  logger.debug("[PinnedManager] Could not refresh session title:", err);
413
415
  }
414
416
  }
417
+ /**
418
+ * Refresh current project name and git branch.
419
+ */
420
+ async refreshProjectMetadata() {
421
+ const project = getCurrentProject();
422
+ this.state.projectName =
423
+ project?.name || this.extractProjectName(project?.worktree) || t("pinned.unknown");
424
+ this.state.projectBranch = await this.getGitBranchName(project?.worktree);
425
+ }
426
+ /**
427
+ * Resolve current git branch for a project worktree.
428
+ */
429
+ async getGitBranchName(worktree) {
430
+ if (!worktree) {
431
+ return null;
432
+ }
433
+ try {
434
+ const gitDir = await this.resolveGitDir(worktree);
435
+ if (!gitDir) {
436
+ return null;
437
+ }
438
+ const headPath = path.join(gitDir, "HEAD");
439
+ const headContent = (await readFile(headPath, "utf-8")).trim();
440
+ const match = headContent.match(/^ref:\s+refs\/heads\/(.+)$/);
441
+ return match?.[1] || null;
442
+ }
443
+ catch (err) {
444
+ logger.debug("[PinnedManager] Could not resolve git branch:", err);
445
+ return null;
446
+ }
447
+ }
448
+ /**
449
+ * Resolve git directory for a normal repository or linked worktree.
450
+ */
451
+ async resolveGitDir(worktree) {
452
+ const gitPath = path.join(worktree, ".git");
453
+ try {
454
+ const gitStat = await stat(gitPath);
455
+ if (gitStat.isDirectory()) {
456
+ return gitPath;
457
+ }
458
+ if (!gitStat.isFile()) {
459
+ return null;
460
+ }
461
+ const gitPointer = (await readFile(gitPath, "utf-8")).trim();
462
+ const match = gitPointer.match(/^gitdir:\s*(.+)$/i);
463
+ if (!match) {
464
+ return null;
465
+ }
466
+ return path.resolve(worktree, match[1].trim());
467
+ }
468
+ catch {
469
+ return null;
470
+ }
471
+ }
415
472
  /**
416
473
  * Extract project name from worktree path
417
474
  */
@@ -482,9 +539,12 @@ class PinnedMessageManager {
482
539
  formatMessage() {
483
540
  const currentModel = getStoredModel();
484
541
  const modelName = formatModelDisplayName(currentModel.providerID, currentModel.modelID);
542
+ const projectDisplayName = this.state.projectBranch
543
+ ? `${this.state.projectName}: ${this.state.projectBranch}`
544
+ : this.state.projectName;
485
545
  const lines = [
486
546
  `${this.state.sessionTitle}`,
487
- t("pinned.line.project", { project: this.state.projectName }),
547
+ t("pinned.line.project", { project: projectDisplayName }),
488
548
  t("pinned.line.model", { model: modelName }),
489
549
  formatContextLine(this.state.tokensUsed, this.state.tokensLimit),
490
550
  ];
@@ -650,6 +710,7 @@ class PinnedMessageManager {
650
710
  this.state.sessionId = null;
651
711
  this.state.tokensUsed = 0;
652
712
  this.state.tokensLimit = 0;
713
+ this.state.projectBranch = null;
653
714
  this.state.changedFiles = [];
654
715
  this.lastRenderedMessageText = null;
655
716
  this.pendingUpdate = false;
@@ -665,6 +726,7 @@ class PinnedMessageManager {
665
726
  this.state.sessionId = null;
666
727
  this.state.sessionTitle = t("pinned.default_session_title");
667
728
  this.state.projectName = "";
729
+ this.state.projectBranch = null;
668
730
  this.state.tokensUsed = 0;
669
731
  this.state.tokensLimit = 0;
670
732
  this.state.changedFiles = [];
@@ -63,6 +63,13 @@ export function clearSession() {
63
63
  currentSettings.currentSession = undefined;
64
64
  void writeSettingsFile(currentSettings);
65
65
  }
66
+ export function isTtsEnabled() {
67
+ return currentSettings.ttsEnabled === true;
68
+ }
69
+ export function setTtsEnabled(enabled) {
70
+ currentSettings.ttsEnabled = enabled;
71
+ void writeSettingsFile(currentSettings);
72
+ }
66
73
  export function getCurrentAgent() {
67
74
  return currentSettings.currentAgent;
68
75
  }
@@ -6,6 +6,15 @@ import { t } from "../i18n/index.js";
6
6
  import { getCurrentProject } from "../settings/manager.js";
7
7
  const TELEGRAM_MESSAGE_LIMIT = 4096;
8
8
  const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
9
+ function truncateWithEllipsis(text, maxLength) {
10
+ if (text.length <= maxLength) {
11
+ return text;
12
+ }
13
+ if (maxLength <= 3) {
14
+ return ".".repeat(Math.max(0, maxLength));
15
+ }
16
+ return `${text.slice(0, maxLength - 3).trimEnd()}...`;
17
+ }
9
18
  function endsWithOddTrailingBackslashes(text, start, end) {
10
19
  let backslashCount = 0;
11
20
  for (let index = end - 1; index >= start; index--) {
@@ -380,7 +389,7 @@ export function formatToolInfo(toolInfo) {
380
389
  description = `${input.description}\n`;
381
390
  }
382
391
  if (tool === "bash" && input && typeof input.command === "string") {
383
- details = input.command;
392
+ details = truncateWithEllipsis(input.command, config.bot.bashToolDisplayMaxLength);
384
393
  }
385
394
  if (tool === "apply_patch") {
386
395
  const filediff = toolInfo.metadata && "filediff" in toolInfo.metadata
@@ -0,0 +1,59 @@
1
+ import { config } from "../config.js";
2
+ import { logger } from "../utils/logger.js";
3
+ const TTS_REQUEST_TIMEOUT_MS = 60_000;
4
+ export function isTtsConfigured() {
5
+ return Boolean(config.tts.apiUrl && config.tts.apiKey);
6
+ }
7
+ export async function synthesizeSpeech(text) {
8
+ if (!isTtsConfigured()) {
9
+ throw new Error("TTS is not configured: set TTS API credentials");
10
+ }
11
+ const input = text.trim();
12
+ if (!input) {
13
+ throw new Error("TTS input text is empty");
14
+ }
15
+ const controller = new AbortController();
16
+ const timeout = setTimeout(() => controller.abort(), TTS_REQUEST_TIMEOUT_MS);
17
+ try {
18
+ const url = `${config.tts.apiUrl}/audio/speech`;
19
+ logger.debug(`[TTS] Sending speech synthesis request: url=${url}, model=${config.tts.model}, voice=${config.tts.voice}, chars=${input.length}`);
20
+ const response = await fetch(url, {
21
+ method: "POST",
22
+ headers: {
23
+ Authorization: `Bearer ${config.tts.apiKey}`,
24
+ "Content-Type": "application/json",
25
+ },
26
+ body: JSON.stringify({
27
+ model: config.tts.model,
28
+ voice: config.tts.voice,
29
+ input,
30
+ response_format: "mp3",
31
+ }),
32
+ signal: controller.signal,
33
+ });
34
+ if (!response.ok) {
35
+ const errorBody = await response.text().catch(() => "");
36
+ throw new Error(`TTS API returned HTTP ${response.status}: ${errorBody || response.statusText}`);
37
+ }
38
+ const arrayBuffer = await response.arrayBuffer();
39
+ const buffer = Buffer.from(arrayBuffer);
40
+ if (buffer.length === 0) {
41
+ throw new Error("TTS API returned an empty audio response");
42
+ }
43
+ logger.debug(`[TTS] Generated speech audio: ${buffer.length} bytes`);
44
+ return {
45
+ buffer,
46
+ filename: "assistant-reply.mp3",
47
+ mimeType: "audio/mpeg",
48
+ };
49
+ }
50
+ catch (err) {
51
+ if (err instanceof DOMException && err.name === "AbortError") {
52
+ throw new Error(`TTS request timed out after ${TTS_REQUEST_TIMEOUT_MS}ms`);
53
+ }
54
+ throw err;
55
+ }
56
+ finally {
57
+ clearTimeout(timeout);
58
+ }
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.14.1",
3
+ "version": "0.15.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",