@grinev/opencode-telegram-bot 0.21.0 → 0.21.2

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.
Files changed (182) hide show
  1. package/.env.example +18 -2
  2. package/README.md +21 -8
  3. package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
  4. package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
  5. package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
  6. package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +4 -132
  7. package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
  8. package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
  9. package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
  10. package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
  11. package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
  12. package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
  13. package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
  14. package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
  15. package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
  16. package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
  17. package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +3 -3
  18. package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
  19. package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
  20. package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
  21. package/dist/app/services/command-catalog-service.js +21 -0
  22. package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
  23. package/dist/app/services/file-browser-service.js +265 -0
  24. package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
  25. package/dist/app/services/mcp-catalog-service.js +79 -0
  26. package/dist/app/services/message-history-service.js +81 -0
  27. package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
  28. package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
  29. package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
  30. package/dist/{project/manager.js → app/services/project-service.js} +3 -3
  31. package/dist/app/services/project-switch-service.js +39 -0
  32. package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
  33. package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +5 -5
  34. package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
  35. package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
  36. package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
  37. package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
  38. package/dist/{session/manager.js → app/services/session-service.js} +1 -1
  39. package/dist/app/services/skills-catalog-service.js +21 -0
  40. package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
  41. package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
  42. package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
  43. package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
  44. package/dist/{settings/manager.js → app/stores/settings-store.js} +3 -3
  45. package/dist/app/types/worktree.js +1 -0
  46. package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
  47. package/dist/bot/callbacks/callback-router.js +110 -0
  48. package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
  49. package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
  50. package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
  51. package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
  52. package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
  53. package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
  54. package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
  55. package/dist/bot/callbacks/permission-callback-handler.js +114 -0
  56. package/dist/bot/callbacks/project-callback-handler.js +79 -0
  57. package/dist/bot/callbacks/question-callback-handler.js +159 -0
  58. package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
  59. package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
  60. package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
  61. package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  62. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  63. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  64. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  65. package/dist/bot/commands/command-catalog-command.js +42 -0
  66. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  67. package/dist/bot/commands/ls-command.js +48 -0
  68. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  69. package/dist/bot/commands/messages-command.js +59 -0
  70. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  71. package/dist/bot/commands/open-command.js +45 -0
  72. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  73. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  74. package/dist/bot/commands/projects-command.js +32 -0
  75. package/dist/bot/commands/rename-command.js +33 -0
  76. package/dist/bot/commands/sessions-command.js +42 -0
  77. package/dist/bot/commands/skills-catalog-command.js +42 -0
  78. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  79. package/dist/bot/commands/{status.js → status-command.js} +9 -9
  80. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  81. package/dist/bot/commands/tasklist-command.js +40 -0
  82. package/dist/bot/commands/{tts.js → tts-command.js} +2 -2
  83. package/dist/bot/commands/worktree-command.js +47 -0
  84. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  85. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  86. package/dist/bot/handlers/photo-handler.js +44 -0
  87. package/dist/bot/handlers/prompt.js +16 -16
  88. package/dist/bot/handlers/text-message-handler.js +61 -0
  89. package/dist/bot/handlers/tts-response-handler.js +29 -0
  90. package/dist/bot/handlers/{voice.js → voice-handler.js} +2 -2
  91. package/dist/bot/index.js +27 -1130
  92. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  93. package/dist/bot/keyboards/keyboard-types.js +1 -0
  94. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  95. package/dist/bot/menus/agent-selection-menu.js +54 -0
  96. package/dist/bot/menus/command-catalog-menu.js +101 -0
  97. package/dist/bot/menus/context-control-menu.js +33 -0
  98. package/dist/bot/menus/file-browser-menu.js +265 -0
  99. package/dist/bot/{handlers → menus}/inline-menu.js +4 -33
  100. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  101. package/dist/bot/menus/message-history-menu.js +107 -0
  102. package/dist/bot/menus/model-selection-menu.js +66 -0
  103. package/dist/bot/menus/permission-menu.js +116 -0
  104. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  105. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  106. package/dist/bot/menus/rename-menu.js +6 -0
  107. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  108. package/dist/bot/menus/session-selection-menu.js +115 -0
  109. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  110. package/dist/bot/menus/variant-selection-menu.js +67 -0
  111. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  112. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  113. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  114. package/dist/bot/messages/external-user-input-notification.js +22 -0
  115. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  116. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  117. package/dist/bot/messages/summary-message-formatter.js +130 -0
  118. package/dist/bot/{utils → messages}/telegram-text.js +60 -0
  119. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  120. package/dist/bot/middleware/interaction-guard.js +2 -2
  121. package/dist/bot/middleware/unknown-command.js +1 -1
  122. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  123. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  124. package/dist/bot/pinned/pinned-message-types.js +1 -0
  125. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  126. package/dist/bot/render/types.js +1 -0
  127. package/dist/bot/routers/command-router.js +73 -0
  128. package/dist/bot/routers/message-router.js +150 -0
  129. package/dist/bot/services/attach-presentation.js +40 -0
  130. package/dist/bot/services/event-subscription-service.js +842 -0
  131. package/dist/bot/services/project-switch-presentation.js +26 -0
  132. package/dist/bot/streaming/response-streamer.js +18 -2
  133. package/dist/cli.js +4 -4
  134. package/dist/config.js +21 -3
  135. package/dist/i18n/ar.js +491 -0
  136. package/dist/i18n/index.js +7 -0
  137. package/dist/index.js +1 -1
  138. package/dist/opencode/ready-refresh.js +2 -2
  139. package/dist/{service → runtime/service}/manager.js +2 -2
  140. package/dist/runtime/service/types.js +1 -0
  141. package/package.json +1 -1
  142. package/dist/bot/commands/ls.js +0 -445
  143. package/dist/bot/commands/open.js +0 -319
  144. package/dist/bot/commands/worktree.js +0 -201
  145. package/dist/bot/handlers/permission.js +0 -235
  146. package/dist/bot/utils/browser-roots.js +0 -140
  147. package/dist/bot/utils/file-tree.js +0 -92
  148. package/dist/bot/utils/send-tts-response.js +0 -37
  149. package/dist/bot/utils/switch-project.js +0 -45
  150. package/dist/bot/utils/telegram-file-url.js +0 -11
  151. package/dist/interaction/busy.js +0 -8
  152. package/dist/interaction/cleanup.js +0 -32
  153. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  154. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  155. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  156. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  157. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  158. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  159. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  160. /package/dist/{model/types.js → app/types/model.js} +0 -0
  161. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  162. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  163. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  164. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  165. /package/dist/{question/types.js → app/types/session.js} +0 -0
  166. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  167. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  168. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  169. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  170. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  171. /package/dist/bot/{utils → messages}/thinking-message.js +0 -0
  172. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  173. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  174. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  175. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  176. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  177. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  178. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  179. /package/dist/{telegram → bot}/render/validator.js +0 -0
  180. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  181. /package/dist/bot/{utils → streaming}/finalize-assistant-response.js +0 -0
  182. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { config } from "../config.js";
2
- import { t } from "../i18n/index.js";
3
- import { opencodeClient } from "../opencode/client.js";
4
- import { logger } from "../utils/logger.js";
5
- import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./session-ignore.js";
1
+ import { config } from "../../config.js";
2
+ import { t } from "../../i18n/index.js";
3
+ import { opencodeClient } from "../../opencode/client.js";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./scheduled-task-session-ignore-service.js";
6
6
  export const SCHEDULED_TASK_AGENT = "build";
7
7
  const SCHEDULED_TASK_SESSION_TITLE = "Scheduled task run";
8
8
  const EXECUTION_POLL_INTERVAL_MS = 2000;
@@ -1,40 +1,16 @@
1
- import { config } from "../config.js";
2
- import { escapePlainTextForTelegramMarkdownV2, formatSummaryWithMode, } from "../summary/formatter.js";
3
- import { t } from "../i18n/index.js";
4
- import { logger } from "../utils/logger.js";
5
- import { safeBackgroundTask } from "../utils/safe-background-task.js";
6
- import { sendBotText } from "../bot/utils/telegram-text.js";
7
- import { formatAssistantRunFooter } from "../bot/utils/assistant-run-footer.js";
8
- import { executeScheduledTask, SCHEDULED_TASK_AGENT } from "./executor.js";
9
- import { foregroundSessionState } from "./foreground-state.js";
10
- import { cleanupScheduledTaskSessionIgnores } from "./session-ignore.js";
11
- import { computeNextRunAt, isTaskDue } from "./next-run.js";
12
- import { getScheduledTask, listScheduledTasks, removeScheduledTask, replaceScheduledTasks, updateScheduledTask, } from "./store.js";
1
+ import { config } from "../../config.js";
2
+ import { t } from "../../i18n/index.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
5
+ import { formatAssistantRunFooter } from "../formatters/assistant-run-footer-formatter.js";
6
+ import { executeScheduledTask, SCHEDULED_TASK_AGENT } from "./scheduled-task-executor-service.js";
7
+ import { foregroundSessionState } from "../managers/foreground-session-state-manager.js";
8
+ import { cleanupScheduledTaskSessionIgnores } from "./scheduled-task-session-ignore-service.js";
9
+ import { computeNextRunAt, isTaskDue } from "./scheduled-task-next-run-service.js";
10
+ import { getScheduledTask, listScheduledTasks, removeScheduledTask, replaceScheduledTasks, updateScheduledTask, } from "../stores/scheduled-task-store.js";
13
11
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
14
- const TELEGRAM_MESSAGE_LIMIT = 4096;
15
12
  const TASK_DESCRIPTION_PREVIEW_LENGTH = 64;
16
13
  const RESTART_INTERRUPTED_ERROR = "Interrupted by bot restart during scheduled task execution.";
17
- function getScheduledTaskDeliveryFormat() {
18
- return config.bot.messageFormatMode === "markdown" ? "markdown_v2" : "raw";
19
- }
20
- function buildScheduledTaskSuccessMessageParts(delivery) {
21
- if (!delivery.resultText) {
22
- return [delivery.notificationText];
23
- }
24
- if (config.bot.messageFormatMode !== "markdown") {
25
- return formatSummaryWithMode(`${delivery.notificationText}\n\n${delivery.resultText}`, config.bot.messageFormatMode);
26
- }
27
- const header = escapePlainTextForTelegramMarkdownV2(delivery.notificationText);
28
- const resultParts = formatSummaryWithMode(delivery.resultText, config.bot.messageFormatMode);
29
- if (resultParts.length === 0) {
30
- return [header];
31
- }
32
- const firstPart = `${header}\n\n${resultParts[0]}`;
33
- if (firstPart.length <= TELEGRAM_MESSAGE_LIMIT) {
34
- return [firstPart, ...resultParts.slice(1)];
35
- }
36
- return [header, ...resultParts];
37
- }
38
14
  function normalizeTaskPrompt(prompt) {
39
15
  const normalized = prompt.replace(/\s+/g, " ").trim();
40
16
  if (normalized.length <= TASK_DESCRIPTION_PREVIEW_LENGTH) {
@@ -85,14 +61,16 @@ function buildErrorDelivery(task, runAt, errorMessage) {
85
61
  export class ScheduledTaskRuntime {
86
62
  botApi = null;
87
63
  chatId = null;
64
+ deliverySender = null;
88
65
  initialized = false;
89
66
  timersByTaskId = new Map();
90
67
  runningTaskIds = new Set();
91
68
  deliveryQueue = [];
92
69
  flushInProgress = false;
93
- async initialize(bot) {
70
+ async initialize(bot, deliverySender) {
94
71
  this.botApi = bot.api;
95
72
  this.chatId = config.telegram.allowedUserId;
73
+ this.deliverySender = deliverySender ?? null;
96
74
  if (this.initialized) {
97
75
  return;
98
76
  }
@@ -153,6 +131,7 @@ export class ScheduledTaskRuntime {
153
131
  }
154
132
  this.botApi = null;
155
133
  this.chatId = null;
134
+ this.deliverySender = null;
156
135
  this.initialized = false;
157
136
  this.timersByTaskId.clear();
158
137
  this.runningTaskIds.clear();
@@ -370,28 +349,10 @@ export class ScheduledTaskRuntime {
370
349
  return false;
371
350
  }
372
351
  try {
373
- const messageParts = delivery.status === "success"
374
- ? buildScheduledTaskSuccessMessageParts(delivery)
375
- : [delivery.notificationText];
376
- const format = delivery.status === "success" ? getScheduledTaskDeliveryFormat() : "raw";
377
- const suppressResultNotification = delivery.status === "success" && Boolean(delivery.footerText);
378
- for (const part of messageParts) {
379
- await sendBotText({
380
- api: this.botApi,
381
- chatId: this.chatId,
382
- text: part,
383
- format,
384
- ...(suppressResultNotification ? { options: { disable_notification: true } } : {}),
385
- });
386
- }
387
- if (delivery.status === "success" && delivery.footerText) {
388
- await sendBotText({
389
- api: this.botApi,
390
- chatId: this.chatId,
391
- text: delivery.footerText,
392
- format: "raw",
393
- });
352
+ if (this.deliverySender) {
353
+ return await this.deliverySender.send(delivery);
394
354
  }
355
+ await this.botApi.sendMessage(this.chatId, delivery.notificationText);
395
356
  return true;
396
357
  }
397
358
  catch (error) {
@@ -1,6 +1,6 @@
1
- import { opencodeClient } from "../opencode/client.js";
2
- import { logger } from "../utils/logger.js";
3
- import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./session-ignore.js";
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./scheduled-task-session-ignore-service.js";
4
4
  const SCHEDULE_PARSE_SESSION_TITLE = "Scheduled task schedule parser";
5
5
  function getLocalTimezone() {
6
6
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -1,4 +1,4 @@
1
- import { getScheduledTaskSessionIgnores, setScheduledTaskSessionIgnores, } from "../settings/manager.js";
1
+ import { getScheduledTaskSessionIgnores, setScheduledTaskSessionIgnores, } from "../stores/settings-store.js";
2
2
  const SCHEDULED_TASK_SESSION_IGNORE_TTL_MS = 24 * 60 * 60 * 1000;
3
3
  let mutationQueue = Promise.resolve();
4
4
  function isFreshIgnore(ignore, nowMs) {
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import Database from "better-sqlite3";
4
- import { opencodeClient } from "../opencode/client.js";
5
- import { getSessionDirectoryCache, setSessionDirectoryCache } from "../settings/manager.js";
6
- import { logger } from "../utils/logger.js";
4
+ import { opencodeClient } from "../../opencode/client.js";
5
+ import { getSessionDirectoryCache, setSessionDirectoryCache } from "../stores/settings-store.js";
6
+ import { logger } from "../../utils/logger.js";
7
7
  const CACHE_VERSION = 1;
8
8
  const INITIAL_WARMUP_LIMIT = 1000;
9
9
  const INCREMENTAL_SYNC_LIMIT = 1000;
@@ -1,4 +1,4 @@
1
- import { getCurrentSession as getSettingsSession, setCurrentSession as setSettingsSession, clearSession as clearSettingsSession, } from "../settings/manager.js";
1
+ import { getCurrentSession as getSettingsSession, setCurrentSession as setSettingsSession, clearSession as clearSettingsSession, } from "../stores/settings-store.js";
2
2
  export function setCurrentSession(sessionInfo) {
3
3
  setSettingsSession(sessionInfo);
4
4
  }
@@ -0,0 +1,21 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ function normalizeDirectoryForCommandApi(directory) {
3
+ return directory.replace(/\\/g, "/");
4
+ }
5
+ export async function loadSkillsCatalog(projectDirectory) {
6
+ const { data, error } = await opencodeClient.command.list({
7
+ directory: normalizeDirectoryForCommandApi(projectDirectory),
8
+ });
9
+ if (error || !data) {
10
+ throw error || new Error("No skill data received");
11
+ }
12
+ return data
13
+ .filter((skill) => {
14
+ const source = skill.source;
15
+ return typeof skill.name === "string" && skill.name.trim().length > 0 && source === "skill";
16
+ })
17
+ .map((skill) => ({
18
+ name: skill.name,
19
+ description: skill.description,
20
+ }));
21
+ }
@@ -1,5 +1,5 @@
1
- import { config } from "../config.js";
2
- import { logger } from "../utils/logger.js";
1
+ import { config } from "../../config.js";
2
+ import { logger } from "../../utils/logger.js";
3
3
  const STT_REQUEST_TIMEOUT_MS = 60_000;
4
4
  /**
5
5
  * Returns true if STT is configured (API URL and API key are set).
@@ -1,7 +1,8 @@
1
- import { config } from "../config.js";
2
- import { logger } from "../utils/logger.js";
1
+ import { config } from "../../config.js";
2
+ import { logger } from "../../utils/logger.js";
3
3
  import textToSpeech from "@google-cloud/text-to-speech";
4
4
  const TTS_REQUEST_TIMEOUT_MS = 60_000;
5
+ const MAX_TTS_INPUT_CHARS = 4_000;
5
6
  export function isTtsConfigured() {
6
7
  if (config.tts.provider === "google") {
7
8
  return Boolean(process.env.GOOGLE_APPLICATION_CREDENTIALS);
@@ -104,11 +105,51 @@ async function synthesizeWithOpenAi(text) {
104
105
  clearTimeout(timeout);
105
106
  }
106
107
  }
108
+ async function synthesizeWithElevenLabs(text) {
109
+ const apiUrl = config.tts.apiUrl.replace(/\/+$/, "");
110
+ const voiceId = config.tts.voice || "21m00Tcm4TlvDq8ikWAM";
111
+ const url = `${apiUrl}/text-to-speech/${encodeURIComponent(voiceId)}`;
112
+ logger.debug(`[TTS] ElevenLabs: url=${url}, model=${config.tts.model}, voice=${voiceId}, chars=${text.length}`);
113
+ const controller = new AbortController();
114
+ const timeout = setTimeout(() => controller.abort(), TTS_REQUEST_TIMEOUT_MS);
115
+ try {
116
+ const response = await fetch(url, {
117
+ method: "POST",
118
+ headers: {
119
+ "xi-api-key": config.tts.apiKey,
120
+ "Content-Type": "application/json",
121
+ Accept: "audio/mpeg",
122
+ },
123
+ body: JSON.stringify({
124
+ text,
125
+ model_id: config.tts.model || "eleven_flash_v2_5",
126
+ }),
127
+ signal: controller.signal,
128
+ });
129
+ if (!response.ok) {
130
+ const errorBody = await response.text().catch(() => "");
131
+ throw new Error(`TTS API returned HTTP ${response.status}: ${errorBody || response.statusText}`);
132
+ }
133
+ const buffer = Buffer.from(await response.arrayBuffer());
134
+ if (buffer.length === 0) {
135
+ throw new Error("ElevenLabs TTS API returned an empty audio response");
136
+ }
137
+ logger.debug(`[TTS] Generated ElevenLabs speech audio: ${buffer.length} bytes`);
138
+ return { buffer, filename: "assistant-reply.mp3", mimeType: "audio/mpeg" };
139
+ }
140
+ finally {
141
+ clearTimeout(timeout);
142
+ }
143
+ }
107
144
  // --- Public API ---
108
145
  function getNotConfiguredMessage() {
109
- return config.tts.provider === "google"
110
- ? "TTS is not configured: set GOOGLE_APPLICATION_CREDENTIALS for Google Cloud TTS"
111
- : "TTS is not configured: set TTS_API_URL and TTS_API_KEY";
146
+ if (config.tts.provider === "google") {
147
+ return "TTS is not configured: set GOOGLE_APPLICATION_CREDENTIALS for Google Cloud TTS";
148
+ }
149
+ if (config.tts.provider === "elevenlabs") {
150
+ return "TTS is not configured: set TTS_API_URL and TTS_API_KEY for ElevenLabs";
151
+ }
152
+ return "TTS is not configured: set TTS_API_URL and TTS_API_KEY";
112
153
  }
113
154
  export async function synthesizeSpeech(text) {
114
155
  if (!isTtsConfigured()) {
@@ -123,6 +164,9 @@ export async function synthesizeSpeech(text) {
123
164
  if (config.tts.provider === "google") {
124
165
  return await synthesizeWithGoogle(input);
125
166
  }
167
+ if (config.tts.provider === "elevenlabs") {
168
+ return await synthesizeWithElevenLabs(input);
169
+ }
126
170
  return await synthesizeWithOpenAi(input);
127
171
  }
128
172
  catch (err) {
@@ -132,3 +176,22 @@ export async function synthesizeSpeech(text) {
132
176
  throw err;
133
177
  }
134
178
  }
179
+ export async function prepareTtsResponseForSession({ sessionId, text, consumeResponseMode, isTtsConfigured: isTtsConfiguredImpl = isTtsConfigured, synthesizeSpeech: synthesizeSpeechImpl = synthesizeSpeech, }) {
180
+ const responseMode = consumeResponseMode(sessionId);
181
+ if (responseMode !== "text_and_tts") {
182
+ return { shouldSend: false };
183
+ }
184
+ const normalizedText = text.trim();
185
+ if (!normalizedText) {
186
+ return { shouldSend: false };
187
+ }
188
+ if (!isTtsConfiguredImpl()) {
189
+ logger.info(`[TTS] Skipping audio reply for session ${sessionId}: TTS is not configured`);
190
+ return { shouldSend: false };
191
+ }
192
+ if (normalizedText.length > MAX_TTS_INPUT_CHARS) {
193
+ logger.warn(`[TTS] Skipping audio reply for session ${sessionId}: text length ${normalizedText.length} exceeds limit ${MAX_TTS_INPUT_CHARS}`);
194
+ return { shouldSend: false };
195
+ }
196
+ return { shouldSend: true, speech: await synthesizeSpeechImpl(normalizedText) };
197
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Variant Manager - manages model variants (reasoning modes)
3
3
  */
4
- import { opencodeClient } from "../opencode/client.js";
5
- import { getCurrentModel, setCurrentModel } from "../settings/manager.js";
6
- import { logger } from "../utils/logger.js";
4
+ import { opencodeClient } from "../../opencode/client.js";
5
+ import { getCurrentModel, setCurrentModel } from "../stores/settings-store.js";
6
+ import { logger } from "../../utils/logger.js";
7
7
  /**
8
8
  * Get available variants for a model from OpenCode API
9
9
  * @param providerID Provider ID
@@ -1,6 +1,6 @@
1
- import { getScheduledTasks, setScheduledTasks } from "../settings/manager.js";
2
- import { logger } from "../utils/logger.js";
3
- import { cloneScheduledTask } from "./types.js";
1
+ import { getScheduledTasks, setScheduledTasks } from "./settings-store.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { cloneScheduledTask } from "../types/scheduled-task.js";
4
4
  let scheduledTaskMutationQueue = Promise.resolve();
5
5
  async function mutateScheduledTasks(mutator) {
6
6
  const runMutation = async () => {
@@ -1,7 +1,7 @@
1
- import { cloneScheduledTask } from "../scheduled-task/types.js";
2
1
  import path from "node:path";
3
- import { getRuntimePaths } from "../runtime/paths.js";
4
- import { logger } from "../utils/logger.js";
2
+ import { cloneScheduledTask } from "../types/scheduled-task.js";
3
+ import { getRuntimePaths } from "../../runtime/paths.js";
4
+ import { logger } from "../../utils/logger.js";
5
5
  function cloneScheduledTasks(tasks) {
6
6
  return tasks?.map((task) => cloneScheduledTask(task));
7
7
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,14 +1,13 @@
1
- import { InlineKeyboard } from "grammy";
2
- import { selectAgent, getAvailableAgents, fetchCurrentAgent } from "../../agent/manager.js";
3
- import { getAgentDisplayName } from "../../agent/types.js";
4
- import { getStoredModel } from "../../model/manager.js";
5
- import { formatVariantForButton } from "../../variant/manager.js";
1
+ import { selectAgent } from "../../app/services/agent-selection-service.js";
2
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
3
+ import { formatVariantForButton } from "../../app/services/variant-selection-service.js";
4
+ import { getAgentDisplayName } from "../../app/types/agent.js";
6
5
  import { logger } from "../../utils/logger.js";
7
- import { createMainKeyboard } from "../utils/keyboard.js";
8
- import { pinnedMessageManager } from "../../pinned/manager.js";
9
- import { keyboardManager } from "../../keyboard/manager.js";
10
- import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
11
6
  import { t } from "../../i18n/index.js";
7
+ import { createMainKeyboard } from "../keyboards/main-reply-keyboard.js";
8
+ import { keyboardManager } from "../keyboards/keyboard-manager.js";
9
+ import { pinnedMessageManager } from "../pinned/pinned-message-manager.js";
10
+ import { clearActiveInlineMenu, ensureActiveInlineMenu } from "../menus/inline-menu.js";
12
11
  /**
13
12
  * Handle agent selection callback
14
13
  * @param ctx grammY context
@@ -67,51 +66,3 @@ export async function handleAgentSelect(ctx) {
67
66
  return false;
68
67
  }
69
68
  }
70
- /**
71
- * Build inline keyboard with available agents
72
- * @param currentAgent Current agent name for highlighting
73
- * @returns InlineKeyboard with agent selection buttons
74
- */
75
- export async function buildAgentSelectionMenu(currentAgent) {
76
- const keyboard = new InlineKeyboard();
77
- const agents = await getAvailableAgents();
78
- if (agents.length === 0) {
79
- logger.warn("[AgentHandler] No available agents found");
80
- return keyboard;
81
- }
82
- // Add button for each agent
83
- agents.forEach((agent) => {
84
- const isActive = agent.name === currentAgent;
85
- const label = isActive
86
- ? `✅ ${getAgentDisplayName(agent.name)}`
87
- : getAgentDisplayName(agent.name);
88
- keyboard.text(label, `agent:${agent.name}`).row();
89
- });
90
- return keyboard;
91
- }
92
- /**
93
- * Show agent selection menu
94
- * @param ctx grammY context
95
- */
96
- export async function showAgentSelectionMenu(ctx) {
97
- try {
98
- const currentAgent = await fetchCurrentAgent();
99
- const keyboard = await buildAgentSelectionMenu(currentAgent);
100
- if (keyboard.inline_keyboard.length === 0) {
101
- await ctx.reply(t("agent.menu.empty"));
102
- return;
103
- }
104
- const text = currentAgent
105
- ? t("agent.menu.current", { name: getAgentDisplayName(currentAgent) })
106
- : t("agent.menu.select");
107
- await replyWithInlineMenu(ctx, {
108
- menuKind: "agent",
109
- text,
110
- keyboard,
111
- });
112
- }
113
- catch (err) {
114
- logger.error("[AgentHandler] Error showing agent menu:", err);
115
- await ctx.reply(t("agent.menu.error"));
116
- }
117
- }
@@ -0,0 +1,110 @@
1
+ import { clearAllInteractionState } from "../../app/managers/interaction-manager.js";
2
+ import { t } from "../../i18n/index.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { handleAgentSelect } from "./agent-selection-callback-handler.js";
5
+ import { handleCommandsCallback } from "./command-catalog-callback-handler.js";
6
+ import { handleCompactConfirm } from "./context-control-callback-handler.js";
7
+ import { handleLsCallback, handleOpenCallback } from "./file-browser-callback-handler.js";
8
+ import { handleInlineMenuCancel } from "./inline-menu-cancel-callback-handler.js";
9
+ import { handleMcpsCallback } from "./mcp-catalog-callback-handler.js";
10
+ import { handleMessagesCallback } from "./message-history-callback-handler.js";
11
+ import { handleModelSearchCallback, handleModelSearchResults, handleModelSelect, } from "./model-selection-callback-handler.js";
12
+ import { handlePermissionCallback } from "./permission-callback-handler.js";
13
+ import { handleProjectSelect } from "./project-callback-handler.js";
14
+ import { handleQuestionCallback } from "./question-callback-handler.js";
15
+ import { handleRenameCancel } from "./rename-callback-handler.js";
16
+ import { handleBackgroundSessionOpen, handleSessionSelect, } from "./session-callback-handler.js";
17
+ import { handleSkillsCallback } from "./skills-catalog-callback-handler.js";
18
+ import { handleTaskCallback, handleTaskListCallback, } from "./scheduled-task-callback-handler.js";
19
+ import { handleVariantSelect } from "./variant-selection-callback-handler.js";
20
+ import { handleWorktreeCallback } from "./worktree-callback-handler.js";
21
+ import { clearLsPathIndex, clearOpenPathIndex } from "../menus/file-browser-menu.js";
22
+ export function registerCallbackRouter(bot, deps) {
23
+ bot.on("callback_query:data", async (ctx) => {
24
+ logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
25
+ logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
26
+ if (ctx.chat) {
27
+ deps.setTelegramContext(bot, ctx.chat.id);
28
+ }
29
+ try {
30
+ const handledBackgroundSession = await handleBackgroundSessionOpen(ctx, {
31
+ bot,
32
+ ensureEventSubscription: deps.ensureEventSubscription,
33
+ });
34
+ const handledInlineCancel = await handleInlineMenuCancel(ctx);
35
+ if (handledInlineCancel) {
36
+ clearOpenPathIndex();
37
+ clearLsPathIndex();
38
+ }
39
+ const handledSession = await handleSessionSelect(ctx, {
40
+ bot,
41
+ ensureEventSubscription: deps.ensureEventSubscription,
42
+ });
43
+ const handledProject = await handleProjectSelect(ctx, {
44
+ ensureEventSubscription: deps.ensureEventSubscription,
45
+ });
46
+ const handledWorktree = await handleWorktreeCallback(ctx, {
47
+ ensureEventSubscription: deps.ensureEventSubscription,
48
+ });
49
+ const handledOpen = await handleOpenCallback(ctx, {
50
+ ensureEventSubscription: deps.ensureEventSubscription,
51
+ });
52
+ const handledLs = await handleLsCallback(ctx);
53
+ const handledQuestion = await handleQuestionCallback(ctx);
54
+ const handledPermission = await handlePermissionCallback(ctx);
55
+ const handledAgent = await handleAgentSelect(ctx);
56
+ const handledModelSearch = await handleModelSearchCallback(ctx);
57
+ const handledModelSearchResults = await handleModelSearchResults(ctx);
58
+ const handledModel = await handleModelSelect(ctx);
59
+ const handledVariant = await handleVariantSelect(ctx);
60
+ const handledCompactConfirm = await handleCompactConfirm(ctx);
61
+ const handledTask = await handleTaskCallback(ctx);
62
+ const handledTaskList = await handleTaskListCallback(ctx);
63
+ const handledRenameCancel = await handleRenameCancel(ctx);
64
+ const handledCommands = await handleCommandsCallback(ctx, {
65
+ bot,
66
+ ensureEventSubscription: deps.ensureEventSubscription,
67
+ });
68
+ const handledMessages = await handleMessagesCallback(ctx, {
69
+ bot,
70
+ ensureEventSubscription: deps.ensureEventSubscription,
71
+ });
72
+ const handledSkills = await handleSkillsCallback(ctx, {
73
+ bot,
74
+ ensureEventSubscription: deps.ensureEventSubscription,
75
+ });
76
+ const handledMcps = await handleMcpsCallback(ctx);
77
+ logger.debug(`[Bot] Callback handled: backgroundSession=${handledBackgroundSession}, inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, ls=${handledLs}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, modelSearch=${handledModelSearch}, modelSearchResults=${handledModelSearchResults}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, messages=${handledMessages}, skills=${handledSkills}, mcps=${handledMcps}`);
78
+ if (!handledBackgroundSession &&
79
+ !handledInlineCancel &&
80
+ !handledSession &&
81
+ !handledProject &&
82
+ !handledWorktree &&
83
+ !handledOpen &&
84
+ !handledLs &&
85
+ !handledQuestion &&
86
+ !handledPermission &&
87
+ !handledAgent &&
88
+ !handledModelSearch &&
89
+ !handledModelSearchResults &&
90
+ !handledModel &&
91
+ !handledVariant &&
92
+ !handledCompactConfirm &&
93
+ !handledTask &&
94
+ !handledTaskList &&
95
+ !handledRenameCancel &&
96
+ !handledCommands &&
97
+ !handledMessages &&
98
+ !handledSkills &&
99
+ !handledMcps) {
100
+ logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
101
+ await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
102
+ }
103
+ }
104
+ catch (err) {
105
+ logger.error("[Bot] Error handling callback:", err);
106
+ clearAllInteractionState("callback_handler_error");
107
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
108
+ }
109
+ });
110
+ }