@link-assistant/hive-mind 2.13.1 โ†’ 2.13.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.
@@ -31,6 +31,9 @@
31
31
  import { extractSessionIdFromText } from './telegram-log-command.lib.mjs';
32
32
  import { parseGitHubUrl } from './github.lib.mjs';
33
33
  import { cleanNonPrintableChars } from './telegram-markdown.lib.mjs';
34
+ // Issue #2166: every reply/edit in this module goes through the one send funnel
35
+ // (validated, logged, plain-text fallback) even when a caller does not inject it.
36
+ import { safeReply as defaultSafeReply, safeEditMessageText as defaultSafeEditMessageText } from './telegram-safe-reply.lib.mjs';
34
37
 
35
38
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
36
39
 
@@ -146,7 +149,9 @@ function findFirstIssueOrPullUrl(text) {
146
149
  if (!word) continue;
147
150
  const parsed = parseGitHubUrl(word);
148
151
  if (parsed.valid && (parsed.type === 'issue' || parsed.type === 'pull')) {
149
- return parsed.normalized;
152
+ // Issue #2166: match on what the bot interprets, not on the fragment the
153
+ // user happened to copy along with the link.
154
+ return parsed.canonical || parsed.normalized;
150
155
  }
151
156
  }
152
157
  return null;
@@ -203,14 +208,17 @@ export function extractStopTarget(text, repliedTo) {
203
208
  * @returns {Promise<boolean>} true when the card was edited
204
209
  * @see https://github.com/link-assistant/hive-mind/issues/1783
205
210
  */
206
- export async function updateQueueCardForCancellation(item, url, tool, stopperName) {
211
+ export async function updateQueueCardForCancellation(item, url, tool, stopperName, { safeEditMessageText = defaultSafeEditMessageText, verbose = false } = {}) {
207
212
  if (!item || !item.messageInfo || !item.ctx) return false;
208
213
  const toolSuffix = tool ? ` from \`${tool}\` queue` : '';
209
214
  const stopperSuffix = stopperName ? ` by ${stopperName}` : '';
210
215
  const text = `๐Ÿ—‘ *Cancelled*\n\n${url}\n\nRemoved${toolSuffix}${stopperSuffix} via /stop.`;
211
216
  try {
212
217
  const { chatId, messageId } = item.messageInfo;
213
- await item.ctx.telegram.editMessageText(chatId, messageId, undefined, text, { parse_mode: 'Markdown' });
218
+ // Issue #2166: go through the shared safe funnel when the caller provides
219
+ // it, so a card whose URL contains Markdown-significant characters is
220
+ // still updated (as plain text) instead of failing silently.
221
+ await safeEditMessageText(item.ctx.telegram, chatId, messageId, undefined, text, { verbose });
214
222
  // Match the consumer's contract: once a card reaches a terminal state we
215
223
  // forget the message coordinates so nothing else tries to edit it.
216
224
  item.messageInfo = null;
@@ -270,7 +278,10 @@ export function isStopTargetRequester({ userId, queueItem = null, sessionInfo =
270
278
  * See https://github.com/link-assistant/hive-mind/issues/1871.
271
279
  */
272
280
  export function registerStartStopCommands(bot, options) {
273
- const { VERBOSE = false, isOldMessage, isForwarded, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, getSolveQueue } = options;
281
+ // Issue #2166: every reply goes through the shared safe-send funnel (pre-send
282
+ // validation, audit log, plain-text fallback). The defaults keep the module
283
+ // usable in isolation (tests) without a bot wired up.
284
+ const { VERBOSE = false, isOldMessage, isForwarded, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, getSolveQueue, safeReply = defaultSafeReply, safeEditMessageText = defaultSafeEditMessageText } = options;
274
285
  const stopIsolatedSessionImpl = options.stopIsolatedSession || (async (...args) => (await import('./isolation-runner.lib.mjs')).stopIsolatedSession(...args));
275
286
  // Issue #1783: look the UUID up in the session monitor so /stop can let the
276
287
  // user who started the task stop it (mirrors /terminal_watch from PR #1779).
@@ -344,25 +355,25 @@ export function registerStartStopCommands(bot, options) {
344
355
  if (!isGroupChat(ctx)) {
345
356
  if (opts.allowPrivate) return { valid: false, isPrivate: true };
346
357
  VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: not a group chat`);
347
- await ctx.reply(`โŒ The ${cmdName} command only works in group chats.`, { reply_to_message_id: ctx.message.message_id });
358
+ await safeReply(ctx, `โŒ The ${cmdName} command only works in group chats.`, { reply_to_message_id: ctx.message.message_id });
348
359
  return { valid: false };
349
360
  }
350
361
  const chatId = ctx.chat.id;
351
362
  if (!isChatAuthorized(chatId)) {
352
363
  VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: chat not authorized`);
353
- await ctx.reply(`โŒ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
364
+ await safeReply(ctx, `โŒ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
354
365
  return { valid: false };
355
366
  }
356
367
  try {
357
368
  const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
358
369
  if (chatMember.status !== 'creator') {
359
370
  VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: user is not chat owner`);
360
- await ctx.reply('โŒ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
371
+ await safeReply(ctx, 'โŒ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
361
372
  return { valid: false };
362
373
  }
363
374
  } catch (error) {
364
375
  console.error('[ERROR] Failed to check chat member status:', error);
365
- await ctx.reply('โŒ Failed to verify permissions.', { reply_to_message_id: ctx.message.message_id });
376
+ await safeReply(ctx, 'โŒ Failed to verify permissions.', { reply_to_message_id: ctx.message.message_id });
366
377
  return { valid: false };
367
378
  }
368
379
  VERBOSE && console.log(`[VERBOSE] ${cmdName} passed all checks`);
@@ -395,13 +406,13 @@ export function registerStartStopCommands(bot, options) {
395
406
  const chatType = ctx.chat?.type;
396
407
  if (chatType === 'private') return true;
397
408
  if (!isGroupChat(ctx)) {
398
- await ctx.reply('โŒ The /stop command only works in group chats or private chats with the bot.', { reply_to_message_id: message.message_id });
409
+ await safeReply(ctx, 'โŒ The /stop command only works in group chats or private chats with the bot.', { reply_to_message_id: message.message_id });
399
410
  return false;
400
411
  }
401
412
  if (!isChatAuthorized(chatId)) {
402
413
  if (!isTopicAuthorized || !isTopicAuthorized(ctx)) {
403
414
  const errMsg = buildAuthErrorMessage ? buildAuthErrorMessage(ctx) : `โŒ This chat (ID: ${chatId}) is not authorized to use this bot.`;
404
- await ctx.reply(errMsg, { reply_to_message_id: message.message_id });
415
+ await safeReply(ctx, errMsg, { reply_to_message_id: message.message_id });
405
416
  return false;
406
417
  }
407
418
  }
@@ -415,12 +426,12 @@ export function registerStartStopCommands(bot, options) {
415
426
  const member = await ctx.telegram.getChatMember(chatId, ctx.from.id);
416
427
  if (!member || member.status !== 'creator') {
417
428
  VERBOSE && console.log(`[VERBOSE] /stop <${label}> ignored: user is not chat owner or task requester`);
418
- await ctx.reply(`โŒ /stop <${label}> is only available to the chat owner or the user who started this task.`, { reply_to_message_id: message.message_id });
429
+ await safeReply(ctx, `โŒ /stop <${label}> is only available to the chat owner or the user who started this task.`, { reply_to_message_id: message.message_id });
419
430
  return false;
420
431
  }
421
432
  } catch (error) {
422
433
  console.error(`[ERROR] /stop <${label}>: getChatMember failed:`, error);
423
- await ctx.reply('โŒ Failed to verify permissions for /stop.', { reply_to_message_id: message.message_id });
434
+ await safeReply(ctx, 'โŒ Failed to verify permissions for /stop.', { reply_to_message_id: message.message_id });
424
435
  return false;
425
436
  }
426
437
  return true;
@@ -438,7 +449,7 @@ export function registerStartStopCommands(bot, options) {
438
449
  */
439
450
  async function runStopIsolatedSessionFlow(ctx, sessionId) {
440
451
  const message = ctx.message;
441
- const ack = await ctx.reply(`โน๏ธ Asking session \`${sessionId}\` to stop (sending CTRL+C via \`$ --stop\`)โ€ฆ`, {
452
+ const ack = await safeReply(ctx, `โน๏ธ Asking session \`${sessionId}\` to stop (sending CTRL+C via \`$ --stop\`)โ€ฆ`, {
442
453
  parse_mode: 'Markdown',
443
454
  reply_to_message_id: message.message_id,
444
455
  });
@@ -481,10 +492,10 @@ export function registerStartStopCommands(bot, options) {
481
492
  }
482
493
 
483
494
  try {
484
- await ctx.telegram.editMessageText(ack.chat.id, ack.message_id, undefined, lines.join('\n'), { parse_mode: 'Markdown' });
495
+ await safeEditMessageText(ctx.telegram, ack.chat.id, ack.message_id, undefined, lines.join('\n'), { verbose: VERBOSE });
485
496
  } catch (error) {
486
497
  console.error('[ERROR] /stop: editMessageText failed, falling back to reply:', error);
487
- await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
498
+ await safeReply(ctx, lines.join('\n'), { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
488
499
  }
489
500
  }
490
501
 
@@ -621,13 +632,13 @@ export function registerStartStopCommands(bot, options) {
621
632
  // running-but-non-stoppable (non-isolation) session, say so; otherwise
622
633
  // fall back to the UUID hint.
623
634
  if (runningSession) {
624
- await ctx.reply(`โš ๏ธ Found a running task for ${url}, but it was not started with an isolation backend, so \`/stop\` cannot forward CTRL+C to it.\n\nNext time run it with the default isolation backend or pass \`--isolation docker\` to make this task interruptible via \`/stop\`.`, {
635
+ await safeReply(ctx, `โš ๏ธ Found a running task for ${url}, but it was not started with an isolation backend, so \`/stop\` cannot forward CTRL+C to it.\n\nNext time run it with the default isolation backend or pass \`--isolation docker\` to make this task interruptible via \`/stop\`.`, {
625
636
  parse_mode: 'Markdown',
626
637
  reply_to_message_id: message.message_id,
627
638
  });
628
639
  return;
629
640
  }
630
- await ctx.reply(`โ„น๏ธ Cannot look up tasks by URL right now (the bot has no solve queue available in this context).\n\nIf you have the session UUID, you can use \`/stop <UUID>\` instead.`, {
641
+ await safeReply(ctx, `โ„น๏ธ Cannot look up tasks by URL right now (the bot has no solve queue available in this context).\n\nIf you have the session UUID, you can use \`/stop <UUID>\` instead.`, {
631
642
  parse_mode: 'Markdown',
632
643
  reply_to_message_id: message.message_id,
633
644
  });
@@ -639,13 +650,13 @@ export function registerStartStopCommands(bot, options) {
639
650
  // have forwarded CTRL+C above). If it tracked a non-isolation session,
640
651
  // explain why it can't be stopped; otherwise report not found.
641
652
  if (runningSession) {
642
- await ctx.reply(`โš ๏ธ Found a running task for ${url}, but it was not started with an isolation backend, so \`/stop\` cannot forward CTRL+C to it.\n\nNext time run it with the default isolation backend or pass \`--isolation docker\` to make this task interruptible via \`/stop\`.`, {
653
+ await safeReply(ctx, `โš ๏ธ Found a running task for ${url}, but it was not started with an isolation backend, so \`/stop\` cannot forward CTRL+C to it.\n\nNext time run it with the default isolation backend or pass \`--isolation docker\` to make this task interruptible via \`/stop\`.`, {
643
654
  parse_mode: 'Markdown',
644
655
  reply_to_message_id: message.message_id,
645
656
  });
646
657
  return;
647
658
  }
648
- await ctx.reply(`โ„น๏ธ No queued or running task found for ${url}.\n\nIf the task is running with an isolation backend, try \`/stop <UUID>\` (the UUID is shown in the bot's session-id message).`, {
659
+ await safeReply(ctx, `โ„น๏ธ No queued or running task found for ${url}.\n\nIf the task is running with an isolation backend, try \`/stop <UUID>\` (the UUID is shown in the bot's session-id message).`, {
649
660
  parse_mode: 'Markdown',
650
661
  reply_to_message_id: message.message_id,
651
662
  });
@@ -661,9 +672,9 @@ export function registerStartStopCommands(bot, options) {
661
672
  // coordinates on the item itself when the card was first posted โ€”
662
673
  // see telegram-bot.mjs where `item.messageInfo` is wired up.
663
674
  const stopperName = ctx.from?.username ? `@${ctx.from.username}` : ctx.from?.first_name || `user ${ctx.from?.id}`;
664
- await updateQueueCardForCancellation(lookup.item, url, lookup.tool, stopperName);
675
+ await updateQueueCardForCancellation(lookup.item, url, lookup.tool, stopperName, { safeEditMessageText, verbose: VERBOSE });
665
676
 
666
- await ctx.reply(`๐Ÿ—‘ Removed queued task for ${url}${toolLabel}.`, {
677
+ await safeReply(ctx, `๐Ÿ—‘ Removed queued task for ${url}${toolLabel}.`, {
667
678
  parse_mode: 'Markdown',
668
679
  reply_to_message_id: message.message_id,
669
680
  });
@@ -679,7 +690,7 @@ export function registerStartStopCommands(bot, options) {
679
690
  // running-not-isolated: a started, non-isolated screen session. We
680
691
  // could shell out to `screen -X -S <name> stuff $'\003'`, but that's
681
692
  // brittle and out of scope for #1780. Tell the user how to recover.
682
- await ctx.reply(`โš ๏ธ Found a running task for ${url}, but it was not started with an isolation backend, so \`/stop\` cannot forward CTRL+C to it.\n\nNext time run it with the default isolation backend or pass \`--isolation docker\` to make this task interruptible via \`/stop\`.`, {
693
+ await safeReply(ctx, `โš ๏ธ Found a running task for ${url}, but it was not started with an isolation backend, so \`/stop\` cannot forward CTRL+C to it.\n\nNext time run it with the default isolation backend or pass \`--isolation docker\` to make this task interruptible via \`/stop\`.`, {
683
694
  parse_mode: 'Markdown',
684
695
  reply_to_message_id: message.message_id,
685
696
  });
@@ -707,7 +718,7 @@ export function registerStartStopCommands(bot, options) {
707
718
  alreadyStoppedMsg += `\nReason: ${stopInfo.reason}`;
708
719
  }
709
720
  alreadyStoppedMsg += '\n\nUse /start to resume accepting tasks.';
710
- await ctx.reply(alreadyStoppedMsg, {
721
+ await safeReply(ctx, alreadyStoppedMsg, {
711
722
  reply_to_message_id: ctx.message.message_id,
712
723
  });
713
724
  return;
@@ -739,7 +750,7 @@ export function registerStartStopCommands(bot, options) {
739
750
  }
740
751
  stopMessage += '*Disabled commands:*\n' + 'โ€ข /solve - No new issues will be accepted\n' + 'โ€ข /hive - No new hive commands will be accepted\n' + 'โ€ข /merge - No new merge operations will be accepted\n\n' + '*Still available:*\n' + 'โ€ข /help - Show help\n' + 'โ€ข /limits - Show usage limits\n' + 'โ€ข /version - Show version info\n' + 'โ€ข /start - Resume accepting tasks (owner only)\n\n' + '๐Ÿ’ก Any tasks already in queue will continue to process.';
741
752
 
742
- await ctx.reply(stopMessage, {
753
+ await safeReply(ctx, stopMessage, {
743
754
  parse_mode: 'Markdown',
744
755
  reply_to_message_id: ctx.message.message_id,
745
756
  });
@@ -755,7 +766,7 @@ export function registerStartStopCommands(bot, options) {
755
766
  // In private chats, show a welcome message instead
756
767
  if (check.isPrivate) {
757
768
  VERBOSE && console.log('[VERBOSE] /start in private chat: showing welcome');
758
- await ctx.reply('๐Ÿ‘‹ *Welcome to SwarmMindBot!*\n\n' + 'This bot helps solve GitHub issues using AI.\n\n' + 'To use this bot:\n' + '1. Add me to a group chat\n' + '2. Make me an admin\n' + '3. Use /solve to solve GitHub issues\n\n' + 'Use /help in a group chat for more information.', { parse_mode: 'Markdown' });
769
+ await safeReply(ctx, '๐Ÿ‘‹ *Welcome to SwarmMindBot!*\n\n' + 'This bot helps solve GitHub issues using AI.\n\n' + 'To use this bot:\n' + '1. Add me to a group chat\n' + '2. Make me an admin\n' + '3. Use /solve to solve GitHub issues\n\n' + 'Use /help in a group chat for more information.', { parse_mode: 'Markdown' });
759
770
  }
760
771
  return;
761
772
  }
@@ -763,7 +774,7 @@ export function registerStartStopCommands(bot, options) {
763
774
 
764
775
  // Check if already running (not stopped)
765
776
  if (!isChatStopped(chatId)) {
766
- await ctx.reply('โ„น๏ธ Bot is already accepting tasks in this chat.\n\nUse /help to see available commands.', {
777
+ await safeReply(ctx, 'โ„น๏ธ Bot is already accepting tasks in this chat.\n\nUse /help to see available commands.', {
767
778
  reply_to_message_id: ctx.message.message_id,
768
779
  });
769
780
  return;
@@ -791,7 +802,7 @@ export function registerStartStopCommands(bot, options) {
791
802
  }
792
803
  }
793
804
 
794
- await ctx.reply('โœ… *Bot Started*\n\n' + 'This bot is now accepting tasks in this chat.\n\n' + (durationStr ? `Bot was stopped for ${durationStr}.\n\n` : '') + 'Use /help to see available commands.', {
805
+ await safeReply(ctx, 'โœ… *Bot Started*\n\n' + 'This bot is now accepting tasks in this chat.\n\n' + (durationStr ? `Bot was stopped for ${durationStr}.\n\n` : '') + 'Use /help to see available commands.', {
795
806
  parse_mode: 'Markdown',
796
807
  reply_to_message_id: ctx.message.message_id,
797
808
  });
@@ -14,6 +14,8 @@
14
14
  * @see https://github.com/link-assistant/hive-mind/issues/1688
15
15
  */
16
16
 
17
+ import { safeReply } from './telegram-safe-reply.lib.mjs';
18
+
17
19
  // Map<userId, { username, firstName, subscribedAt, sourceChatId }>
18
20
  const subscribers = new Map();
19
21
 
@@ -196,9 +198,9 @@ export function registerSubscribeCommands(bot, options = {}) {
196
198
  message = 'โ„น๏ธ You are already subscribed.\n\nUse /unsubscribe to stop receiving private notifications when /solve commands finish.';
197
199
  }
198
200
 
199
- await ctx.reply(message, {
200
- parse_mode: 'Markdown',
201
+ await safeReply(ctx, message, {
201
202
  reply_to_message_id: ctx.message?.message_id,
203
+ verbose: VERBOSE,
202
204
  });
203
205
 
204
206
  VERBOSE && console.log(`[VERBOSE] Subscriber ${userId} (${ctx.from.username || ctx.from.first_name || 'unknown'}) added (new=${wasNew}); total=${getSubscriberCount()}`);
@@ -210,9 +212,9 @@ export function registerSubscribeCommands(bot, options = {}) {
210
212
  const userId = ctx.from.id;
211
213
  const wasRemoved = removeSubscriber(userId);
212
214
 
213
- await ctx.reply(wasRemoved ? UNSUBSCRIBE_CONFIRMATION : NOT_SUBSCRIBED_MESSAGE, {
214
- parse_mode: 'Markdown',
215
+ await safeReply(ctx, wasRemoved ? UNSUBSCRIBE_CONFIRMATION : NOT_SUBSCRIBED_MESSAGE, {
215
216
  reply_to_message_id: ctx.message?.message_id,
217
+ verbose: VERBOSE,
216
218
  });
217
219
 
218
220
  VERBOSE && console.log(`[VERBOSE] Subscriber ${userId} (${ctx.from.username || ctx.from.first_name || 'unknown'}) removed=${wasRemoved}; total=${getSubscriberCount()}`);
@@ -8,6 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import { extractSessionIdFromText, decideLogDestination, resolveLogPath } from './telegram-log-command.lib.mjs';
10
10
  import { parseSessionExitFooter } from './isolation-runner.lib.mjs';
11
+ import { safeReply, safeSendMessage, safeEditMessageText } from './telegram-safe-reply.lib.mjs';
11
12
  import { classifyExitStatus, isFailureSessionStatus } from './session-status.lib.mjs';
12
13
 
13
14
  const DEFAULT_WIDTH = 120;
@@ -248,7 +249,7 @@ export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, log
248
249
  const message = formatTerminalWatchMessage({ sessionId, statusResult, logText, options, updateCount, completed, repoDescription });
249
250
  const shouldEdit = !lastMessage || snapshotChanged || (completed && message !== lastMessage);
250
251
  if (shouldEdit && message !== lastMessage) {
251
- await bot.telegram.editMessageText(chatId, messageId, undefined, message, { parse_mode: 'Markdown' });
252
+ await safeEditMessageText(bot.telegram, chatId, messageId, undefined, message, { verbose });
252
253
  lastMessage = message;
253
254
  }
254
255
  lastSnapshot = snapshot;
@@ -281,9 +282,9 @@ function buildUsage() {
281
282
 
282
283
  async function createWatchMessage({ ctx, targetChatId, replyToMessageId, text }) {
283
284
  if (targetChatId === ctx.chat.id) {
284
- return await ctx.reply(text, { parse_mode: 'Markdown', reply_to_message_id: replyToMessageId });
285
+ return await safeReply(ctx, text, { reply_to_message_id: replyToMessageId });
285
286
  }
286
- return await ctx.telegram.sendMessage(targetChatId, text, replyToMessageId ? { parse_mode: 'Markdown', reply_to_message_id: replyToMessageId } : { parse_mode: 'Markdown' });
287
+ return await safeSendMessage(ctx.telegram, targetChatId, text, replyToMessageId ? { reply_to_message_id: replyToMessageId } : {});
287
288
  }
288
289
 
289
290
  async function forwardOrCopyToDm(ctx, sourceMessage) {
@@ -324,7 +325,7 @@ async function startWatchFromResolvedSession({ bot, ctx, sessionId, statusResult
324
325
  watchTerminalLogSession({ bot, chatId: targetChatId, messageId: watchMessage.message_id, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options: watchOptions, repoDescription, verbose, initialStatusResult: statusResult, initialLogText, initialMessage: initialText });
325
326
 
326
327
  if (!auto && decision.destination === 'dm' && ctx.chat.type !== 'private') {
327
- await ctx.reply(`๐Ÿ“ฌ Started terminal watch for \`${sessionId}\` in your direct messages.`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
328
+ await safeReply(ctx, `๐Ÿ“ฌ Started terminal watch for \`${sessionId}\` in your direct messages.`, { reply_to_message_id: ctx.message.message_id, verbose });
328
329
  }
329
330
  return { started: true, messageId: watchMessage.message_id, sessionInfo };
330
331
  }
@@ -371,13 +372,13 @@ export async function registerTerminalWatchCommand(bot, options) {
371
372
 
372
373
  const parsedArgs = parseTerminalWatchArgs(message.text || '');
373
374
  if (parsedArgs.errors.length > 0) {
374
- await ctx.reply(`โŒ Invalid /terminal_watch options:\n${parsedArgs.errors.map(e => `โ€ข ${e}`).join('\n')}\n\n${buildUsage()}`, { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
375
+ await safeReply(ctx, `โŒ Invalid /terminal_watch options:\n${parsedArgs.errors.map(e => `โ€ข ${e}`).join('\n')}\n\n${buildUsage()}`, { reply_to_message_id: message.message_id, verbose: VERBOSE });
375
376
  return;
376
377
  }
377
378
 
378
379
  const sessionId = parsedArgs.sessionId || extractSessionIdFromText(message.reply_to_message?.text || message.reply_to_message?.caption || '');
379
380
  if (!sessionId) {
380
- await ctx.reply(`โŒ /terminal_watch requires a session id.\n\n${buildUsage()}`, { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
381
+ await safeReply(ctx, `โŒ /terminal_watch requires a session id.\n\n${buildUsage()}`, { reply_to_message_id: message.message_id, verbose: VERBOSE });
381
382
  return;
382
383
  }
383
384
 
@@ -417,7 +418,7 @@ export async function registerTerminalWatchCommand(bot, options) {
417
418
  }
418
419
 
419
420
  if (!statusResult?.exists) {
420
- await ctx.reply(`โŒ Session \`${sessionId}\` is not known to start-command.`, { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
421
+ await safeReply(ctx, `โŒ Session \`${sessionId}\` is not known to start-command.`, { reply_to_message_id: message.message_id, verbose: VERBOSE });
421
422
  return;
422
423
  }
423
424
 
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { getAllKnownLocalTokens } from './token-sanitization.lib.mjs';
26
26
  import { maskToken } from './lib.mjs';
27
+ import { safeReply } from './telegram-safe-reply.lib.mjs';
27
28
 
28
29
  /**
29
30
  * Resolve allowed chat IDs into an array of numeric IDs the user could own.
@@ -146,7 +147,7 @@ export const registerTokensCommand = (bot, options = {}) => {
146
147
  }
147
148
 
148
149
  const message = formatTokenList(tokens);
149
- await ctx.reply(message, { parse_mode: 'Markdown' });
150
+ await safeReply(ctx, message);
150
151
  });
151
152
  };
152
153
 
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { promisify } from 'util';
18
18
  import { exec as execCallback } from 'child_process';
19
+ import { safeReply, safeEditMessageText } from './telegram-safe-reply.lib.mjs';
19
20
 
20
21
  const exec = promisify(execCallback);
21
22
 
@@ -128,9 +129,9 @@ export function registerTopCommand(bot, options) {
128
129
  }
129
130
 
130
131
  // Show experimental feature warning
131
- await ctx.reply('๐Ÿงช *EXPERIMENTAL FEATURE*\n\nThis command is experimental and may have issues. Use with caution.', {
132
- parse_mode: 'Markdown',
132
+ await safeReply(ctx, '๐Ÿงช *EXPERIMENTAL FEATURE*\n\nThis command is experimental and may have issues. Use with caution.', {
133
133
  reply_to_message_id: ctx.message.message_id,
134
+ verbose: VERBOSE,
134
135
  });
135
136
 
136
137
  // Check if there's already an active top session for this chat
@@ -188,11 +189,11 @@ export function registerTopCommand(bot, options) {
188
189
  const firstOutput = await captureTopOutput(chatId);
189
190
  if (firstOutput) {
190
191
  try {
191
- await ctx.telegram.editMessageText(chatId, initialMessage.message_id, undefined, `\`\`\`\n${firstOutput}\n\`\`\``, {
192
- parse_mode: 'Markdown',
192
+ await safeEditMessageText(ctx.telegram, chatId, initialMessage.message_id, undefined, `\`\`\`\n${firstOutput}\n\`\`\``, {
193
193
  reply_markup: {
194
194
  inline_keyboard: [[{ text: '๐Ÿ›‘ Stop', callback_data: `stop_top_${chatId}` }]],
195
195
  },
196
+ verbose: VERBOSE,
196
197
  });
197
198
  } catch (error) {
198
199
  console.error('[ERROR] Failed to update message:', error);
@@ -204,11 +205,11 @@ export function registerTopCommand(bot, options) {
204
205
  const output = await captureTopOutput(chatId);
205
206
  if (output) {
206
207
  try {
207
- await ctx.telegram.editMessageText(chatId, initialMessage.message_id, undefined, `\`\`\`\n${output}\n\`\`\``, {
208
- parse_mode: 'Markdown',
208
+ await safeEditMessageText(ctx.telegram, chatId, initialMessage.message_id, undefined, `\`\`\`\n${output}\n\`\`\``, {
209
209
  reply_markup: {
210
210
  inline_keyboard: [[{ text: '๐Ÿ›‘ Stop', callback_data: `stop_top_${chatId}` }]],
211
211
  },
212
+ verbose: VERBOSE,
212
213
  });
213
214
  } catch (error) {
214
215
  // Ignore "message is not modified" errors
@@ -290,9 +291,7 @@ export function registerTopCommand(bot, options) {
290
291
 
291
292
  // Update the message to show it's stopped
292
293
  try {
293
- await ctx.editMessageText('๐Ÿ›‘ Top session stopped.', {
294
- parse_mode: 'Markdown',
295
- });
294
+ await safeEditMessageText(ctx.telegram, chatId, ctx.callbackQuery?.message?.message_id, undefined, '๐Ÿ›‘ Top session stopped.', { verbose: VERBOSE });
296
295
  } catch (error) {
297
296
  console.error('[ERROR] Failed to edit message:', error);
298
297
  }