@grinev/opencode-telegram-bot 0.4.0 → 0.6.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.
Files changed (36) hide show
  1. package/.env.example +11 -0
  2. package/README.md +26 -15
  3. package/dist/bot/commands/definitions.js +7 -4
  4. package/dist/bot/commands/help.js +7 -1
  5. package/dist/bot/commands/new.js +4 -0
  6. package/dist/bot/commands/projects.js +18 -9
  7. package/dist/bot/commands/rename.js +49 -1
  8. package/dist/bot/commands/sessions.js +15 -2
  9. package/dist/bot/commands/stop.js +3 -5
  10. package/dist/bot/handlers/agent.js +12 -1
  11. package/dist/bot/handlers/context.js +15 -25
  12. package/dist/bot/handlers/inline-menu.js +119 -0
  13. package/dist/bot/handlers/model.js +15 -2
  14. package/dist/bot/handlers/permission.js +81 -12
  15. package/dist/bot/handlers/question.js +97 -9
  16. package/dist/bot/handlers/variant.js +12 -1
  17. package/dist/bot/index.js +92 -16
  18. package/dist/bot/middleware/interaction-guard.js +80 -0
  19. package/dist/bot/middleware/unknown-command.js +22 -0
  20. package/dist/bot/utils/commands.js +21 -0
  21. package/dist/config.js +31 -0
  22. package/dist/i18n/en.js +23 -4
  23. package/dist/i18n/ru.js +23 -4
  24. package/dist/interaction/cleanup.js +24 -0
  25. package/dist/interaction/guard.js +87 -0
  26. package/dist/interaction/manager.js +106 -0
  27. package/dist/interaction/types.js +1 -0
  28. package/dist/permission/manager.js +60 -38
  29. package/dist/pinned/manager.js +7 -5
  30. package/dist/question/manager.js +33 -0
  31. package/dist/rename/manager.js +3 -0
  32. package/dist/settings/manager.js +6 -1
  33. package/dist/summary/aggregator.js +87 -6
  34. package/dist/summary/formatter.js +91 -15
  35. package/dist/summary/tool-message-batcher.js +182 -0
  36. package/package.json +1 -1
package/dist/bot/index.js CHANGED
@@ -6,6 +6,8 @@ import { SocksProxyAgent } from "socks-proxy-agent";
6
6
  import { HttpsProxyAgent } from "https-proxy-agent";
7
7
  import { config } from "../config.js";
8
8
  import { authMiddleware } from "./middleware/auth.js";
9
+ import { interactionGuardMiddleware } from "./middleware/interaction-guard.js";
10
+ import { unknownCommandMiddleware } from "./middleware/unknown-command.js";
9
11
  import { BOT_COMMANDS } from "./commands/definitions.js";
10
12
  import { startCommand } from "./commands/start.js";
11
13
  import { helpCommand } from "./commands/help.js";
@@ -25,13 +27,16 @@ import { handlePermissionCallback, showPermissionRequest } from "./handlers/perm
25
27
  import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
26
28
  import { handleModelSelect, showModelSelectionMenu } from "./handlers/model.js";
27
29
  import { handleVariantSelect, showVariantSelectionMenu } from "./handlers/variant.js";
28
- import { handleContextButtonPress, handleCompactConfirm, handleCompactCancel, } from "./handlers/context.js";
30
+ import { handleContextButtonPress, handleCompactConfirm } from "./handlers/context.js";
31
+ import { handleInlineMenuCancel } from "./handlers/inline-menu.js";
29
32
  import { questionManager } from "../question/manager.js";
30
- import { permissionManager } from "../permission/manager.js";
33
+ import { interactionManager } from "../interaction/manager.js";
34
+ import { clearAllInteractionState } from "../interaction/cleanup.js";
31
35
  import { keyboardManager } from "../keyboard/manager.js";
32
36
  import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
33
37
  import { summaryAggregator } from "../summary/aggregator.js";
34
38
  import { formatSummary, formatToolInfo } from "../summary/formatter.js";
39
+ import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
35
40
  import { opencodeClient } from "../opencode/client.js";
36
41
  import { clearSession, getCurrentSession, setCurrentSession } from "../session/manager.js";
37
42
  import { ingestSessionInfoForCache } from "../session/cache-manager.js";
@@ -48,6 +53,21 @@ import { t } from "../i18n/index.js";
48
53
  let botInstance = null;
49
54
  let chatIdInstance = null;
50
55
  let commandsInitialized = false;
56
+ const toolMessageBatcher = new ToolMessageBatcher({
57
+ intervalSeconds: 5,
58
+ sendMessage: async (sessionId, text) => {
59
+ if (!botInstance || !chatIdInstance) {
60
+ return;
61
+ }
62
+ const currentSession = getCurrentSession();
63
+ if (!currentSession || currentSession.id !== sessionId) {
64
+ return;
65
+ }
66
+ await botInstance.api.sendMessage(chatIdInstance, text, {
67
+ disable_notification: true,
68
+ });
69
+ },
70
+ });
51
71
  async function ensureCommandsInitialized(ctx, next) {
52
72
  if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
53
73
  await next();
@@ -78,6 +98,10 @@ async function ensureEventSubscription(directory) {
78
98
  logger.error("No directory found for event subscription");
79
99
  return;
80
100
  }
101
+ toolMessageBatcher.setIntervalSeconds(config.bot.serviceMessagesIntervalSec);
102
+ summaryAggregator.setOnCleared(() => {
103
+ toolMessageBatcher.clearAll("summary_aggregator_clear");
104
+ });
81
105
  summaryAggregator.setOnComplete(async (sessionId, messageText) => {
82
106
  if (!botInstance || !chatIdInstance) {
83
107
  logger.error("Bot or chat ID not available for sending message");
@@ -87,6 +111,7 @@ async function ensureEventSubscription(directory) {
87
111
  if (currentSession?.id !== sessionId) {
88
112
  return;
89
113
  }
114
+ await toolMessageBatcher.flushSession(sessionId, "assistant_message_completed");
90
115
  try {
91
116
  const parts = formatSummary(messageText);
92
117
  logger.debug(`[Bot] Sending completed message to Telegram (chatId=${chatIdInstance}, parts=${parts.length})`);
@@ -117,19 +142,21 @@ async function ensureEventSubscription(directory) {
117
142
  }
118
143
  });
119
144
  summaryAggregator.setOnTool(async (toolInfo) => {
145
+ if (config.bot.hideToolCallMessages) {
146
+ return;
147
+ }
120
148
  if (!botInstance || !chatIdInstance) {
121
149
  logger.error("Bot or chat ID not available for sending tool notification");
122
150
  return;
123
151
  }
124
152
  const currentSession = getCurrentSession();
125
- const sessionId = summaryAggregator["currentSessionId"];
126
- if (!currentSession || currentSession.id !== sessionId) {
153
+ if (!currentSession || currentSession.id !== toolInfo.sessionId) {
127
154
  return;
128
155
  }
129
156
  try {
130
157
  const message = formatToolInfo(toolInfo);
131
158
  if (message) {
132
- await botInstance.api.sendMessage(chatIdInstance, message);
159
+ toolMessageBatcher.enqueue(toolInfo.sessionId, message);
133
160
  }
134
161
  }
135
162
  catch (err) {
@@ -155,6 +182,7 @@ async function ensureEventSubscription(directory) {
155
182
  await fs.writeFile(tempFilePath, fileData.buffer);
156
183
  await botInstance.api.sendDocument(chatIdInstance, new InputFile(tempFilePath), {
157
184
  caption: fileData.caption,
185
+ disable_notification: true,
158
186
  });
159
187
  await fs.unlink(tempFilePath);
160
188
  logger.debug(`[Bot] Temporary file deleted: ${fileData.filename}`);
@@ -168,6 +196,18 @@ async function ensureEventSubscription(directory) {
168
196
  logger.error("Bot or chat ID not available for showing questions");
169
197
  return;
170
198
  }
199
+ const currentSession = getCurrentSession();
200
+ if (currentSession) {
201
+ await toolMessageBatcher.flushSession(currentSession.id, "question_asked");
202
+ }
203
+ if (questionManager.isActive()) {
204
+ logger.warn("[Bot] Replacing active poll with a new one");
205
+ const previousMessageIds = questionManager.getMessageIds();
206
+ for (const messageId of previousMessageIds) {
207
+ await botInstance.api.deleteMessage(chatIdInstance, messageId).catch(() => { });
208
+ }
209
+ clearAllInteractionState("question_replaced_by_new_poll");
210
+ }
171
211
  logger.info(`[Bot] Received ${questions.length} questions from agent, requestID=${requestID}`);
172
212
  questionManager.startQuestions(questions, requestID);
173
213
  await showCurrentQuestion(botInstance.api, chatIdInstance);
@@ -183,24 +223,30 @@ async function ensureEventSubscription(directory) {
183
223
  });
184
224
  }
185
225
  }
186
- questionManager.clear();
226
+ clearAllInteractionState("question_error");
187
227
  });
188
228
  summaryAggregator.setOnPermission(async (request) => {
189
229
  if (!botInstance || !chatIdInstance) {
190
230
  logger.error("Bot or chat ID not available for showing permission request");
191
231
  return;
192
232
  }
233
+ await toolMessageBatcher.flushSession(request.sessionID, "permission_asked");
193
234
  logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}`);
194
235
  await showPermissionRequest(botInstance.api, chatIdInstance, request);
195
236
  });
196
- summaryAggregator.setOnThinking(async () => {
237
+ summaryAggregator.setOnThinking(async (sessionId) => {
238
+ if (config.bot.hideThinkingMessages) {
239
+ return;
240
+ }
197
241
  if (!botInstance || !chatIdInstance) {
198
242
  return;
199
243
  }
244
+ const currentSession = getCurrentSession();
245
+ if (!currentSession || currentSession.id !== sessionId) {
246
+ return;
247
+ }
200
248
  logger.debug("[Bot] Agent started thinking");
201
- await botInstance.api.sendMessage(chatIdInstance, t("bot.thinking")).catch((err) => {
202
- logger.error("[Bot] Failed to send thinking message:", err);
203
- });
249
+ toolMessageBatcher.enqueue(sessionId, t("bot.thinking"));
204
250
  });
205
251
  summaryAggregator.setOnTokens(async (tokens) => {
206
252
  if (!pinnedMessageManager.isInitialized()) {
@@ -298,8 +344,7 @@ async function isSessionBusy(sessionId, directory) {
298
344
  async function resetMismatchedSessionContext() {
299
345
  stopEventListening();
300
346
  summaryAggregator.clear();
301
- questionManager.clear();
302
- permissionManager.clear();
347
+ clearAllInteractionState("session_mismatch_reset");
303
348
  clearSession();
304
349
  keyboardManager.clearContext();
305
350
  if (!pinnedMessageManager.isInitialized()) {
@@ -313,6 +358,9 @@ async function resetMismatchedSessionContext() {
313
358
  }
314
359
  }
315
360
  export function createBot() {
361
+ clearAllInteractionState("bot_startup");
362
+ toolMessageBatcher.setIntervalSeconds(config.bot.serviceMessagesIntervalSec);
363
+ logger.info(`[ToolBatcher] Service messages interval: ${config.bot.serviceMessagesIntervalSec}s`);
316
364
  const botOptions = {};
317
365
  if (config.telegram.proxyUrl) {
318
366
  const proxyUrl = config.telegram.proxyUrl;
@@ -365,6 +413,16 @@ export function createBot() {
365
413
  });
366
414
  bot.use(authMiddleware);
367
415
  bot.use(ensureCommandsInitialized);
416
+ bot.use(interactionGuardMiddleware);
417
+ const blockMenuWhileInteractionActive = async (ctx) => {
418
+ const activeInteraction = interactionManager.getSnapshot();
419
+ if (!activeInteraction) {
420
+ return false;
421
+ }
422
+ logger.debug(`[Bot] Blocking menu open while interaction active: kind=${activeInteraction.kind}, expectedInput=${activeInteraction.expectedInput}`);
423
+ await ctx.reply(t("interaction.blocked.finish_current"));
424
+ return true;
425
+ };
368
426
  bot.command("start", startCommand);
369
427
  bot.command("help", helpCommand);
370
428
  bot.command("status", statusCommand);
@@ -377,10 +435,12 @@ export function createBot() {
377
435
  bot.command("model", handleModelCommand);
378
436
  bot.command("stop", stopCommand);
379
437
  bot.command("rename", renameCommand);
438
+ bot.on("message:text", unknownCommandMiddleware);
380
439
  bot.on("callback_query:data", async (ctx) => {
381
440
  logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
382
441
  logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
383
442
  try {
443
+ const handledInlineCancel = await handleInlineMenuCancel(ctx);
384
444
  const handledSession = await handleSessionSelect(ctx);
385
445
  const handledProject = await handleProjectSelect(ctx);
386
446
  const handledQuestion = await handleQuestionCallback(ctx);
@@ -389,10 +449,10 @@ export function createBot() {
389
449
  const handledModel = await handleModelSelect(ctx);
390
450
  const handledVariant = await handleVariantSelect(ctx);
391
451
  const handledCompactConfirm = await handleCompactConfirm(ctx);
392
- const handledCompactCancel = await handleCompactCancel(ctx);
393
452
  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}`);
395
- if (!handledSession &&
453
+ logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, rename=${handledRenameCancel}`);
454
+ if (!handledInlineCancel &&
455
+ !handledSession &&
396
456
  !handledProject &&
397
457
  !handledQuestion &&
398
458
  !handledPermission &&
@@ -400,7 +460,6 @@ export function createBot() {
400
460
  !handledModel &&
401
461
  !handledVariant &&
402
462
  !handledCompactConfirm &&
403
- !handledCompactCancel &&
404
463
  !handledRenameCancel) {
405
464
  logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
406
465
  await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
@@ -408,6 +467,7 @@ export function createBot() {
408
467
  }
409
468
  catch (err) {
410
469
  logger.error("[Bot] Error handling callback:", err);
470
+ clearAllInteractionState("callback_handler_error");
411
471
  await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
412
472
  }
413
473
  });
@@ -415,6 +475,9 @@ export function createBot() {
415
475
  bot.hears(/^(📋|🛠️|💬|🔍|📝|📄|📦|🤖) \w+ Mode$/, async (ctx) => {
416
476
  logger.debug(`[Bot] Agent mode button pressed: ${ctx.message?.text}`);
417
477
  try {
478
+ if (await blockMenuWhileInteractionActive(ctx)) {
479
+ return;
480
+ }
418
481
  await showAgentSelectionMenu(ctx);
419
482
  }
420
483
  catch (err) {
@@ -427,6 +490,9 @@ export function createBot() {
427
490
  bot.hears(MODEL_BUTTON_TEXT_PATTERN, async (ctx) => {
428
491
  logger.debug(`[Bot] Model button pressed: ${ctx.message?.text}`);
429
492
  try {
493
+ if (await blockMenuWhileInteractionActive(ctx)) {
494
+ return;
495
+ }
430
496
  await showModelSelectionMenu(ctx);
431
497
  }
432
498
  catch (err) {
@@ -438,6 +504,9 @@ export function createBot() {
438
504
  bot.hears(/^📊(?:\s|$)/, async (ctx) => {
439
505
  logger.debug(`[Bot] Context button pressed: ${ctx.message?.text}`);
440
506
  try {
507
+ if (await blockMenuWhileInteractionActive(ctx)) {
508
+ return;
509
+ }
441
510
  await handleContextButtonPress(ctx);
442
511
  }
443
512
  catch (err) {
@@ -450,6 +519,9 @@ export function createBot() {
450
519
  bot.hears(VARIANT_BUTTON_TEXT_PATTERN, async (ctx) => {
451
520
  logger.debug(`[Bot] Variant button pressed: ${ctx.message?.text}`);
452
521
  try {
522
+ if (await blockMenuWhileInteractionActive(ctx)) {
523
+ return;
524
+ }
453
525
  await showVariantSelectionMenu(ctx);
454
526
  }
455
527
  catch (err) {
@@ -628,12 +700,16 @@ export function createBot() {
628
700
  }
629
701
  catch (err) {
630
702
  logger.error("Error in prompt handler:", err);
703
+ if (interactionManager.getSnapshot()) {
704
+ clearAllInteractionState("message_handler_error");
705
+ }
631
706
  await ctx.reply(t("error.generic"));
632
707
  }
633
708
  logger.debug("[Bot] message:text handler completed (prompt sent in background)");
634
709
  });
635
710
  bot.catch((err) => {
636
711
  logger.error("[Bot] Unhandled error in bot:", err);
712
+ clearAllInteractionState("bot_unhandled_error");
637
713
  if (err.ctx) {
638
714
  logger.error("[Bot] Error context - update type:", err.ctx.update ? Object.keys(err.ctx.update) : "unknown");
639
715
  }
@@ -0,0 +1,80 @@
1
+ import { resolveInteractionGuardDecision } from "../../interaction/guard.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { t } from "../../i18n/index.js";
4
+ function getInteractionBlockedMessage(reason, interactionKind) {
5
+ if (interactionKind === "permission") {
6
+ switch (reason) {
7
+ case "command_not_allowed":
8
+ return t("permission.blocked.command_not_allowed");
9
+ case "expected_callback":
10
+ case "expected_command":
11
+ case "expected_text":
12
+ default:
13
+ return t("permission.blocked.expected_reply");
14
+ }
15
+ }
16
+ if (interactionKind === "inline") {
17
+ switch (reason) {
18
+ case "command_not_allowed":
19
+ return t("inline.blocked.command_not_allowed");
20
+ case "expected_callback":
21
+ case "expected_command":
22
+ case "expected_text":
23
+ default:
24
+ return t("inline.blocked.expected_choice");
25
+ }
26
+ }
27
+ if (interactionKind === "question") {
28
+ switch (reason) {
29
+ case "command_not_allowed":
30
+ return t("question.blocked.command_not_allowed");
31
+ case "expected_callback":
32
+ case "expected_command":
33
+ case "expected_text":
34
+ default:
35
+ return t("question.blocked.expected_answer");
36
+ }
37
+ }
38
+ if (interactionKind === "rename") {
39
+ switch (reason) {
40
+ case "command_not_allowed":
41
+ return t("rename.blocked.command_not_allowed");
42
+ case "expected_callback":
43
+ case "expected_command":
44
+ case "expected_text":
45
+ default:
46
+ return t("rename.blocked.expected_name");
47
+ }
48
+ }
49
+ switch (reason) {
50
+ case "expired":
51
+ return t("interaction.blocked.expired");
52
+ case "expected_callback":
53
+ return t("interaction.blocked.expected_callback");
54
+ case "expected_command":
55
+ return t("interaction.blocked.expected_command");
56
+ case "command_not_allowed":
57
+ return t("interaction.blocked.command_not_allowed");
58
+ case "expected_text":
59
+ default:
60
+ return t("interaction.blocked.expected_text");
61
+ }
62
+ }
63
+ export async function interactionGuardMiddleware(ctx, next) {
64
+ const decision = resolveInteractionGuardDecision(ctx);
65
+ if (decision.allow) {
66
+ await next();
67
+ return;
68
+ }
69
+ const message = getInteractionBlockedMessage(decision.reason, decision.state?.kind);
70
+ logger.debug(`[InteractionGuard] Blocked input: interactionKind=${decision.state?.kind || "none"}, inputType=${decision.inputType}, reason=${decision.reason || "unknown"}, command=${decision.command || "-"}`);
71
+ if (ctx.callbackQuery) {
72
+ await ctx.answerCallbackQuery({ text: message }).catch(() => { });
73
+ return;
74
+ }
75
+ if (ctx.chat) {
76
+ await ctx.reply(message).catch((err) => {
77
+ logger.error("[InteractionGuard] Failed to send blocked input message:", err);
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,22 @@
1
+ import { extractCommandName, isKnownCommand } from "../utils/commands.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { t } from "../../i18n/index.js";
4
+ export async function unknownCommandMiddleware(ctx, next) {
5
+ const text = ctx.message?.text;
6
+ if (!text) {
7
+ await next();
8
+ return;
9
+ }
10
+ const commandName = extractCommandName(text);
11
+ if (!commandName) {
12
+ await next();
13
+ return;
14
+ }
15
+ if (isKnownCommand(commandName)) {
16
+ await next();
17
+ return;
18
+ }
19
+ const commandToken = text.trim().split(/\s+/)[0];
20
+ logger.debug(`[Bot] Unknown slash command received: ${commandToken}`);
21
+ await ctx.reply(t("bot.unknown_command", { command: commandToken }));
22
+ }
@@ -0,0 +1,21 @@
1
+ import { BOT_COMMANDS } from "../commands/definitions.js";
2
+ const KNOWN_COMMANDS = new Set(["start", ...BOT_COMMANDS.map((item) => item.command)]);
3
+ export function extractCommandName(text) {
4
+ const trimmed = text.trim();
5
+ if (!trimmed.startsWith("/")) {
6
+ return null;
7
+ }
8
+ const token = trimmed.split(/\s+/)[0];
9
+ const withoutSlash = token.slice(1);
10
+ if (!withoutSlash) {
11
+ return null;
12
+ }
13
+ const withoutMention = withoutSlash.split("@")[0].toLowerCase();
14
+ if (!withoutMention) {
15
+ return null;
16
+ }
17
+ return withoutMention;
18
+ }
19
+ export function isKnownCommand(commandName) {
20
+ return KNOWN_COMMANDS.has(commandName);
21
+ }
package/dist/config.js CHANGED
@@ -20,6 +20,20 @@ function getOptionalPositiveIntEnvVar(key, defaultValue) {
20
20
  }
21
21
  return parsedValue;
22
22
  }
23
+ function getOptionalNonNegativeIntEnvVarFromKeys(keys, defaultValue) {
24
+ for (const key of keys) {
25
+ const value = getEnvVar(key, false);
26
+ if (!value) {
27
+ continue;
28
+ }
29
+ const parsedValue = Number.parseInt(value, 10);
30
+ if (Number.isNaN(parsedValue) || parsedValue < 0) {
31
+ return defaultValue;
32
+ }
33
+ return parsedValue;
34
+ }
35
+ return defaultValue;
36
+ }
23
37
  function getOptionalLocaleEnvVar(key, defaultValue) {
24
38
  const value = getEnvVar(key, false);
25
39
  if (!value) {
@@ -34,6 +48,20 @@ function getOptionalLocaleEnvVar(key, defaultValue) {
34
48
  }
35
49
  return defaultValue;
36
50
  }
51
+ function getOptionalBooleanEnvVar(key, defaultValue) {
52
+ const value = getEnvVar(key, false);
53
+ if (!value) {
54
+ return defaultValue;
55
+ }
56
+ const normalized = value.trim().toLowerCase();
57
+ if (["1", "true", "yes", "on"].includes(normalized)) {
58
+ return true;
59
+ }
60
+ if (["0", "false", "no", "off"].includes(normalized)) {
61
+ return false;
62
+ }
63
+ return defaultValue;
64
+ }
37
65
  export const config = {
38
66
  telegram: {
39
67
  token: getEnvVar("TELEGRAM_BOT_TOKEN"),
@@ -55,6 +83,9 @@ export const config = {
55
83
  bot: {
56
84
  sessionsListLimit: getOptionalPositiveIntEnvVar("SESSIONS_LIST_LIMIT", 10),
57
85
  locale: getOptionalLocaleEnvVar("BOT_LOCALE", "en"),
86
+ serviceMessagesIntervalSec: getOptionalNonNegativeIntEnvVarFromKeys(["SERVICE_MESSAGES_INTERVAL_SEC", "TOOL_MESSAGES_INTERVAL_SEC"], 5),
87
+ hideThinkingMessages: getOptionalBooleanEnvVar("HIDE_THINKING_MESSAGES", false),
88
+ hideToolCallMessages: getOptionalBooleanEnvVar("HIDE_TOOL_CALL_MESSAGES", false),
58
89
  },
59
90
  files: {
60
91
  maxFileSizeKb: parseInt(getEnvVar("CODE_FILE_MAX_SIZE_KB", false) || "100", 10),
package/dist/i18n/en.js CHANGED
@@ -16,6 +16,19 @@ export const en = {
16
16
  "error.load_variants": "❌ Failed to load variants list",
17
17
  "error.context_button": "❌ Failed to process context button",
18
18
  "error.generic": "🔴 Something went wrong.",
19
+ "interaction.blocked.expired": "⚠️ This interaction has expired. Please start it again.",
20
+ "interaction.blocked.expected_callback": "⚠️ Please use the inline buttons for this step or tap Cancel.",
21
+ "interaction.blocked.expected_text": "⚠️ Please send a text message for this step.",
22
+ "interaction.blocked.expected_command": "⚠️ Please send a command for this step.",
23
+ "interaction.blocked.command_not_allowed": "⚠️ This command is not available in the current step.",
24
+ "interaction.blocked.finish_current": "⚠️ Finish the current interaction first (answer or cancel), then open another menu.",
25
+ "inline.blocked.expected_choice": "⚠️ Choose an option using the inline buttons or tap Cancel.",
26
+ "inline.blocked.command_not_allowed": "⚠️ This command is not available while inline menu is active.",
27
+ "question.blocked.expected_answer": "⚠️ Answer the current question using buttons, Custom answer, or Cancel.",
28
+ "question.blocked.command_not_allowed": "⚠️ This command is not available until current question flow is completed.",
29
+ "inline.button.cancel": "❌ Cancel",
30
+ "inline.inactive_callback": "This menu is inactive",
31
+ "inline.cancelled_callback": "Cancelled",
19
32
  "common.unknown": "unknown",
20
33
  "common.unknown_error": "unknown error",
21
34
  "start.welcome": "👋 Welcome to OpenCode Telegram Bot!\n\nUse commands:\n/projects — select project\n/sessions — session list\n/new — new session\n/agent — switch mode\n/model — select model\n/status — status\n/help — help",
@@ -29,6 +42,7 @@ export const en = {
29
42
  "bot.session_reset_project_mismatch": "⚠️ Active session does not match the selected project, so it was reset. Use /sessions to pick one or /new to create a new session.",
30
43
  "bot.prompt_send_error_detailed": "🔴 Failed to send request.\n\nDetails: {details}",
31
44
  "bot.prompt_send_error": "🔴 An error occurred while sending request to OpenCode.",
45
+ "bot.unknown_command": "⚠️ Unknown command: {command}. Use /help to see available commands.",
32
46
  "status.header_running": "🟢 **OpenCode Server is running**",
33
47
  "status.health.healthy": "Healthy",
34
48
  "status.health.unhealthy": "Unhealthy",
@@ -111,7 +125,6 @@ export const en = {
111
125
  "variant.menu.current": "Current variant: {name}\n\nSelect variant:",
112
126
  "variant.menu.error": "🔴 Failed to get variants list",
113
127
  "context.button.confirm": "✅ Yes, compact context",
114
- "context.button.cancel": "❌ Cancel",
115
128
  "context.no_active_session": "⚠️ No active session. Create a session with /new",
116
129
  "context.confirm_text": '📊 Context compaction for session "{title}"\n\nThis will reduce context usage by removing old messages from history. Current task will not be interrupted.\n\nContinue?',
117
130
  "context.callback_session_not_found": "Session not found",
@@ -119,7 +132,6 @@ export const en = {
119
132
  "context.progress": "⏳ Compacting context...",
120
133
  "context.error": "❌ Context compaction failed",
121
134
  "context.success": "✅ Context compacted successfully",
122
- "context.callback_cancelled": "Cancelled",
123
135
  "permission.inactive_callback": "Permission request is inactive",
124
136
  "permission.processing_error_callback": "Processing error",
125
137
  "permission.no_active_request_callback": "Error: no active request",
@@ -127,9 +139,11 @@ export const en = {
127
139
  "permission.reply.always": "Always allowed",
128
140
  "permission.reply.reject": "Rejected",
129
141
  "permission.send_reply_error": "❌ Failed to send permission reply",
142
+ "permission.blocked.expected_reply": "⚠️ Please answer the permission request first using the buttons above.",
143
+ "permission.blocked.command_not_allowed": "⚠️ This command is not available until you answer the permission request.",
130
144
  "permission.header": "{emoji} **Permission request: {name}**\n\n",
131
- "permission.button.allow": "✅ Allow",
132
- "permission.button.always": "🔓 Always",
145
+ "permission.button.allow": "✅ Allow once",
146
+ "permission.button.always": "🔓 Allow always",
133
147
  "permission.button.reject": "❌ Reject",
134
148
  "permission.name.bash": "Bash",
135
149
  "permission.name.edit": "Edit",
@@ -156,6 +170,7 @@ export const en = {
156
170
  "question.button.submit": "✅ Done",
157
171
  "question.button.custom": "🔤 Custom answer",
158
172
  "question.button.cancel": "❌ Cancel",
173
+ "question.use_custom_button_first": '⚠️ To send text, tap "Custom answer" for the current question first.',
159
174
  "question.summary.title": "✅ Poll completed!\n\n",
160
175
  "question.summary.question": "Question {index}:\n{question}\n\n",
161
176
  "question.summary.answer": "Answer:\n{answer}\n\n",
@@ -193,6 +208,10 @@ export const en = {
193
208
  "rename.success": "✅ Session renamed to: {title}",
194
209
  "rename.error": "🔴 Failed to rename session.",
195
210
  "rename.cancelled": "❌ Rename cancelled.",
211
+ "rename.inactive_callback": "Rename request is inactive",
212
+ "rename.inactive": "⚠️ Rename request is not active. Run /rename again.",
213
+ "rename.blocked.expected_name": "⚠️ Enter a new session name as text or tap Cancel in rename message.",
214
+ "rename.blocked.command_not_allowed": "⚠️ This command is not available while rename is waiting for a new name.",
196
215
  "rename.button.cancel": "❌ Cancel",
197
216
  "cmd.description.rename": "Rename current session",
198
217
  "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",
package/dist/i18n/ru.js CHANGED
@@ -16,6 +16,19 @@ export const ru = {
16
16
  "error.load_variants": "❌ Ошибка при загрузке списка вариантов",
17
17
  "error.context_button": "❌ Ошибка при обработке кнопки контекста",
18
18
  "error.generic": "🔴 Произошла ошибка.",
19
+ "interaction.blocked.expired": "⚠️ Текущая интеракция устарела. Запустите ее снова.",
20
+ "interaction.blocked.expected_callback": "⚠️ Для этого шага используйте inline-кнопки или нажмите Отмена.",
21
+ "interaction.blocked.expected_text": "⚠️ Для этого шага отправьте текстовое сообщение.",
22
+ "interaction.blocked.expected_command": "⚠️ Для этого шага отправьте команду.",
23
+ "interaction.blocked.command_not_allowed": "⚠️ Эта команда недоступна на текущем шаге.",
24
+ "interaction.blocked.finish_current": "⚠️ Сначала завершите текущую интеракцию (ответьте или отмените), затем откройте другое меню.",
25
+ "inline.blocked.expected_choice": "⚠️ Выберите вариант через inline-кнопки или нажмите Отмена.",
26
+ "inline.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока активно inline-меню.",
27
+ "question.blocked.expected_answer": "⚠️ Ответьте на текущий вопрос кнопками, через Свой ответ, или нажмите Отмена.",
28
+ "question.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока не завершен текущий опрос.",
29
+ "inline.button.cancel": "❌ Отмена",
30
+ "inline.inactive_callback": "Это меню уже неактивно",
31
+ "inline.cancelled_callback": "Отменено",
19
32
  "common.unknown": "неизвестна",
20
33
  "common.unknown_error": "неизвестная ошибка",
21
34
  "start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/agent — сменить режим\n/model — выбрать модель\n/status — статус\n/help — справка",
@@ -29,6 +42,7 @@ export const ru = {
29
42
  "bot.session_reset_project_mismatch": "⚠️ Активная сессия не соответствует выбранному проекту, поэтому была сброшена. Используйте /sessions для выбора или /new для создания новой сессии.",
30
43
  "bot.prompt_send_error_detailed": "🔴 Ошибка при отправке запроса.\n\nДетали: {details}",
31
44
  "bot.prompt_send_error": "🔴 Произошла ошибка при отправке запроса в OpenCode.",
45
+ "bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
32
46
  "status.header_running": "🟢 **OpenCode Server запущен**",
33
47
  "status.health.healthy": "Healthy",
34
48
  "status.health.unhealthy": "Unhealthy",
@@ -111,7 +125,6 @@ export const ru = {
111
125
  "variant.menu.current": "Текущий variant: {name}\n\nВыберите variant:",
112
126
  "variant.menu.error": "🔴 Не удалось получить список вариантов",
113
127
  "context.button.confirm": "✅ Да, сжать контекст",
114
- "context.button.cancel": "❌ Отмена",
115
128
  "context.no_active_session": "⚠️ Нет активной сессии. Создайте сессию командой /new",
116
129
  "context.confirm_text": '📊 Сжатие контекста для сессии "{title}"\n\nЭто уменьшит использование контекста, удалив старые сообщения из истории. Текущая задача не будет прервана.\n\nПродолжить?',
117
130
  "context.callback_session_not_found": "Сессия не найдена",
@@ -119,7 +132,6 @@ export const ru = {
119
132
  "context.progress": "⏳ Сжимаю контекст...",
120
133
  "context.error": "❌ Ошибка при сжатии контекста",
121
134
  "context.success": "✅ Контекст успешно сжат",
122
- "context.callback_cancelled": "Отменено",
123
135
  "permission.inactive_callback": "Запрос разрешения неактивен",
124
136
  "permission.processing_error_callback": "Ошибка при обработке",
125
137
  "permission.no_active_request_callback": "Ошибка: нет активного запроса",
@@ -127,9 +139,11 @@ export const ru = {
127
139
  "permission.reply.always": "Разрешено всегда",
128
140
  "permission.reply.reject": "Отклонено",
129
141
  "permission.send_reply_error": "❌ Не удалось отправить ответ на запрос разрешения",
142
+ "permission.blocked.expected_reply": "⚠️ Сначала ответьте на запрос разрешения кнопками выше.",
143
+ "permission.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока вы не ответите на запрос разрешения.",
130
144
  "permission.header": "{emoji} **Запрос разрешения: {name}**\n\n",
131
- "permission.button.allow": "✅ Разрешить",
132
- "permission.button.always": "🔓 Всегда",
145
+ "permission.button.allow": "✅ Разрешить один раз",
146
+ "permission.button.always": "🔓 Разрешить всегда",
133
147
  "permission.button.reject": "❌ Отклонить",
134
148
  "permission.name.bash": "Bash",
135
149
  "permission.name.edit": "Edit",
@@ -156,6 +170,7 @@ export const ru = {
156
170
  "question.button.submit": "✅ Готово",
157
171
  "question.button.custom": "🔤 Свой ответ",
158
172
  "question.button.cancel": "❌ Отмена",
173
+ "question.use_custom_button_first": '⚠️ Чтобы отправить текст, сначала нажмите кнопку "Свой ответ" для текущего вопроса.',
159
174
  "question.summary.title": "✅ Опрос завершен!\n\n",
160
175
  "question.summary.question": "Вопрос {index}:\n{question}\n\n",
161
176
  "question.summary.answer": "Ответ:\n{answer}\n\n",
@@ -193,6 +208,10 @@ export const ru = {
193
208
  "rename.success": "✅ Сессия переименована в: {title}",
194
209
  "rename.error": "🔴 Не удалось переименовать сессию.",
195
210
  "rename.cancelled": "❌ Переименование отменено.",
211
+ "rename.inactive_callback": "Запрос переименования неактивен",
212
+ "rename.inactive": "⚠️ Запрос переименования неактивен. Выполните /rename снова.",
213
+ "rename.blocked.expected_name": "⚠️ Введите новое название текстом или нажмите Отмена в сообщении переименования.",
214
+ "rename.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока ожидается новое название сессии.",
196
215
  "rename.button.cancel": "❌ Отмена",
197
216
  "cmd.description.rename": "Переименовать текущую сессию",
198
217
  "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`",
@@ -0,0 +1,24 @@
1
+ import { permissionManager } from "../permission/manager.js";
2
+ import { questionManager } from "../question/manager.js";
3
+ import { renameManager } from "../rename/manager.js";
4
+ import { interactionManager } from "./manager.js";
5
+ import { logger } from "../utils/logger.js";
6
+ export function clearAllInteractionState(reason) {
7
+ const questionActive = questionManager.isActive();
8
+ const permissionActive = permissionManager.isActive();
9
+ const renameActive = renameManager.isWaitingForName();
10
+ const interactionSnapshot = interactionManager.getSnapshot();
11
+ questionManager.clear();
12
+ permissionManager.clear();
13
+ renameManager.clear();
14
+ interactionManager.clear(reason);
15
+ const hasAnyActiveState = questionActive || permissionActive || renameActive || interactionSnapshot !== null;
16
+ const message = `[InteractionCleanup] Cleared state: reason=${reason}, ` +
17
+ `questionActive=${questionActive}, permissionActive=${permissionActive}, ` +
18
+ `renameActive=${renameActive}, interactionKind=${interactionSnapshot?.kind || "none"}`;
19
+ if (hasAnyActiveState) {
20
+ logger.info(message);
21
+ return;
22
+ }
23
+ logger.debug(message);
24
+ }