@link-assistant/hive-mind 1.69.3 → 1.69.5

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.
@@ -45,6 +45,7 @@ const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await
45
45
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
46
46
  const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
47
47
  const { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
48
+ const { buildTelegramHelpMessage, buildTelegramInfoBlock, buildSolveQueuedMessage } = await import('./telegram-ui-messages.lib.mjs');
48
49
 
49
50
  const config = yargs(hideBin(process.argv))
50
51
  .usage('Usage: hive-telegram-bot [options]')
@@ -442,12 +443,12 @@ async function getCommandUrlArg(args, createYargsConfig, positionalNames) {
442
443
  }
443
444
 
444
445
  async function validateGitHubUrl(args, options = {}) {
445
- const { allowedTypes = ['issue', 'pull'], commandName = 'solve', createYargsConfig = null, positionalNames = [] } = options;
446
+ const { allowedTypes = ['issue', 'pull'], commandName = 'solve', createYargsConfig = null, positionalNames = [], locale = null } = options;
446
447
  const rawUrl = await getCommandUrlArg(args, createYargsConfig, positionalNames);
447
- if (!rawUrl) return { valid: false, error: `Missing GitHub URL. Usage: /${commandName} <github-url> [options]` };
448
+ if (!rawUrl) return { valid: false, error: t('telegram.missing_github_url', { commandName }, { locale }) };
448
449
  // Issue #1102: Clean non-printable chars (Zero-Width Space, BOM, etc.) from URLs
449
450
  const url = cleanNonPrintableChars(rawUrl);
450
- if (!url.includes('github.com')) return { valid: false, error: 'First argument must be a GitHub URL' };
451
+ if (!url.includes('github.com')) return { valid: false, error: t('telegram.first_arg_must_be_github_url', {}, { locale }) };
451
452
  const parsed = parseGitHubUrl(url);
452
453
  if (!parsed.valid) return { valid: false, error: parsed.error || 'Invalid GitHub URL', suggestion: parsed.suggestion };
453
454
  if (!allowedTypes.includes(parsed.type)) {
@@ -456,10 +457,10 @@ async function validateGitHubUrl(args, options = {}) {
456
457
  const escapedUrl = escapeMarkdown(url),
457
458
  escapedBaseUrl = escapeMarkdown(baseUrl); // Issue #1102: escape for Markdown
458
459
  let error;
459
- if (parsed.type === 'issues_list') error = `URL points to the issues list page, but you need a specific issue\n\nšŸ’” How to fix:\n1. Open the repository: ${escapedUrl}\n2. Click on a specific issue\n3. Copy the URL (it should end with /issues/NUMBER)\n\nExample: \`${escapedBaseUrl}/issues/1\``;
460
- else if (parsed.type === 'pulls_list') error = `URL points to the pull requests list page, but you need a specific pull request\n\nšŸ’” How to fix:\n1. Open the repository: ${escapedUrl}\n2. Click on a specific pull request\n3. Copy the URL (it should end with /pull/NUMBER)\n\nExample: \`${escapedBaseUrl}/pull/1\``;
461
- else if (parsed.type === 'repo') error = `URL points to a repository, but you need a specific ${allowedTypesStr}\n\nšŸ’” How to fix:\n1. Go to: ${escapedUrl}/issues\n2. Click on an issue to solve\n3. Use the full URL with the issue number\n\nExample: \`${escapedBaseUrl}/issues/1\``;
462
- else error = `URL must be a GitHub ${allowedTypesStr} (not ${parsed.type.replace('_', ' ')})`;
460
+ if (parsed.type === 'issues_list') error = t('telegram.url_issues_list_error', { url: escapedUrl, example: `${escapedBaseUrl}/issues/1` }, { locale });
461
+ else if (parsed.type === 'pulls_list') error = t('telegram.url_pulls_list_error', { url: escapedUrl, example: `${escapedBaseUrl}/pull/1` }, { locale });
462
+ else if (parsed.type === 'repo') error = t('telegram.url_repo_error', { allowedTypes: allowedTypesStr, url: escapedUrl, example: `${escapedBaseUrl}/issues/1` }, { locale });
463
+ else error = t('telegram.url_must_be_type', { allowedTypes: allowedTypesStr, type: parsed.type.replace('_', ' ') }, { locale });
463
464
  return { valid: false, error };
464
465
  }
465
466
  return { valid: true, parsed, normalizedUrl: url };
@@ -486,104 +487,32 @@ bot.command('help', async ctx => {
486
487
  const chatType = ctx.chat.type;
487
488
  const chatTitle = ctx.chat.title || 'Private Chat';
488
489
  const topicId = ctx.message?.message_thread_id; // Forum topic ID (issue #1100)
489
- let message = 'šŸ¤– *SwarmMindBot Help*\n\n';
490
-
491
- // Show stopped status if chat is stopped (issue #1081)
492
- if (isChatStopped(chatId)) {
493
- const stopInfo = getChatStopInfo(chatId);
494
- const reason = stopInfo?.reason || DEFAULT_STOP_REASON;
495
- message += 'šŸ›‘ *Bot Status: STOPPED*\n';
496
- message += `Reason: ${reason}\n`;
497
- if (stopInfo?.stoppedAt) {
498
- message += `Stopped: ${stopInfo.stoppedAt.toISOString()}\n`;
499
- }
500
- message += 'Use /start (chat owner only) to resume.\n\n';
501
- }
502
-
503
- message += 'šŸ“‹ *Diagnostic Information:*\n';
504
- message += `• Chat ID: \`${chatId}\`\n`;
505
- if (topicId) message += `• Topic ID: \`${topicId}\`\n`;
506
- message += `• Chat Type: ${chatType}\n`;
507
- message += `• Chat Title: ${chatTitle}\n\n`;
508
- message += 'šŸ“ *Available Commands:*\n\n';
509
-
510
- if (solveEnabled) {
511
- message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - Solve a GitHub issue\n';
512
- message += 'Usage: `/solve <github-url> [options]`\n';
513
- message += 'Example: `/solve https://github.com/owner/repo/issues/123 --model sonnet`\n';
514
- message += 'Tool aliases imply `--tool <tool>`: `/codex <github-url>` equals `/solve <github-url> --tool codex`\n';
515
- message += 'Or reply to a message with a GitHub link: `/solve`\n';
516
- if (solveOverrides.length > 0) {
517
- message += `šŸ”’ Locked options: \`${solveOverrides.join(' ')}\`\n`;
518
- }
519
- message += '\n';
520
- } else {
521
- message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - āŒ Disabled\n\n';
522
- }
523
-
524
- if (taskEnabled) {
525
- message += '*/task* - Create a GitHub issue from a repository link and issue text\n';
526
- message += 'Usage: `/task <github-repository-url>` followed by issue text, or reply with `/task`\n';
527
- message += 'Example: `/task https://github.com/owner/repo` then the issue text on following lines\n';
528
- message += '*/split* - Split a GitHub issue into smaller issues\n';
529
- message += 'Usage: `/split <github-issue-url> [options]` or `/task --split <github-issue-url>`\n';
530
- message += 'Example: `/split https://github.com/owner/repo/issues/123 --split-count 2`\n\n';
531
- } else {
532
- message += '*/task* / */split* - āŒ Disabled\n\n';
533
- }
534
-
535
- if (hiveEnabled) {
536
- message += '*/hive* - Run hive command\n';
537
- message += 'Usage: `/hive <github-url> [options]`\n';
538
- message += 'Example: `/hive https://github.com/owner/repo`\n';
539
- if (hiveOverrides.length > 0) {
540
- message += `šŸ”’ Locked options: \`${hiveOverrides.join(' ')}\`\n`;
541
- }
542
- message += '\n';
543
- } else {
544
- message += '*/hive* - āŒ Disabled\n\n';
545
- }
546
-
547
- message += '`/solve_queue` - Show solve queue status\n';
548
- message += '*/limits* - Show usage limits\n';
549
- message += '*/version* - Show bot and runtime versions\n';
550
- message += '*/language* `[en|ru|zh|hi]` - Set or show your preferred reply language (in-memory only, per-user)\n';
551
- message += '`/accept_invites` - Accept all pending GitHub invitations\n';
552
- message += '*/merge* - Merge queue (experimental)\n';
553
- message += 'Usage: `/merge <github-repo-url>`\n';
554
- message += "Merges all PRs with 'ready' label sequentially.\n";
555
- message += '*/subscribe* / */unsubscribe* - šŸ”” Get private DM forward of /solve completion (experimental, #1688)\n';
556
- message += '*/help* - Show this help message\n';
557
- message += '*/stop* / */start* - Stop or resume accepting new tasks (owner only)\n';
558
- message += '*/stop* `<uuid>` - Send CTRL+C to an isolated solve/hive session (owner only). Also works as a reply to a message containing the UUID.\n';
559
- message += '*/log* - Fetch isolation session log (owner only). Usage: `/log <uuid>` or reply with `/log`\n';
560
- message += '*/terminal\\_watch* - Live-update an isolation session log (owner only). Usage: `/terminal_watch <uuid>` or reply with `/terminal_watch`\n\n';
561
- message += 'šŸ”” *Session Notifications:* Completion notifications are automatic; use /subscribe for private DM forwards.\n';
562
- if (ISOLATION_BACKEND) message += `šŸ”’ *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
563
- message += '\n';
564
- message += 'āš ļø *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /subscribe and /unsubscribe work in private and group chats.\n\n';
565
- message += 'šŸ”§ *Common Options:*\n';
566
- message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
567
- message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
568
- message += '• `--think <level>` - Thinking level (off/low/medium/high/xhigh/max) | `--thinking-budget <num>` - Token budget (0-63999)\n';
569
- message += '• `--verbose` or `-v` - Verbose output | `--attach-logs` - Attach logs to PR\n';
570
- if (SHOW_LIMITS_ENABLED) message += '• `--show-limits` - Experimental: embed Claude/Codex usage at start, end and delta (#594)\n';
571
- message += '\nšŸ’” *Tip:* Many more options available. See full documentation for complete list.\n';
572
-
573
- if (allowedChats || allowedTopics) {
574
- const authorized = isTopicAuthorized(ctx);
575
- message += `\nšŸ”’ *Restricted Mode:* Authorized: ${authorized ? 'āœ… Yes' : 'āŒ No'}`;
576
- if (!authorized && topicId) message += `\nšŸ’” To allow this topic: \`TELEGRAM_ALLOWED_TOPICS="(${chatId} ${topicId})"\``;
577
- }
578
-
579
- message += '\n\nšŸ”§ *Troubleshooting:*\n';
580
- message += 'If bot is not receiving messages:\n';
581
- message += '1. Check privacy mode in @BotFather\n';
582
- message += ' • Send `/setprivacy` to @BotFather\n';
583
- message += ' • Choose "Disable" for your bot\n';
584
- message += ' • Remove bot from group and re-add\n';
585
- message += '2. Or make bot an admin in the group\n';
586
- message += '3. Restart bot with `--verbose` flag for diagnostics';
490
+ const helpLocale = resolveLocaleFromTelegramCtx(ctx);
491
+ const stopped = isChatStopped(chatId);
492
+ const stopInfo = stopped ? getChatStopInfo(chatId) : null;
493
+ const restrictedMode = Boolean(allowedChats || allowedTopics);
494
+ const authorized = restrictedMode ? isTopicAuthorized(ctx) : null;
495
+ const message = buildTelegramHelpMessage({
496
+ locale: helpLocale,
497
+ chatId,
498
+ chatType,
499
+ chatTitle,
500
+ topicId,
501
+ isStopped: stopped,
502
+ stopInfo,
503
+ stopReason: stopInfo?.reason || DEFAULT_STOP_REASON,
504
+ solveEnabled,
505
+ taskEnabled,
506
+ hiveEnabled,
507
+ solveOverrides,
508
+ hiveOverrides,
509
+ showLimitsEnabled: SHOW_LIMITS_ENABLED,
510
+ isolationBackend: ISOLATION_BACKEND,
511
+ modelDescription: buildModelOptionDescription(),
512
+ restrictedMode,
513
+ authorized,
514
+ allowTopicHint: topicId ? `TELEGRAM_ALLOWED_TOPICS="(${chatId} ${topicId})"` : '',
515
+ });
587
516
 
588
517
  await ctx.reply(message, { parse_mode: 'Markdown' });
589
518
  });
@@ -636,8 +565,8 @@ bot.command('limits', async ctx => {
636
565
  const codexError = limits.codex.success ? null : limits.codex.error;
637
566
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
638
567
  const queueStatus = await solveQueue.formatStatus();
639
- const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError);
640
- const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus]);
568
+ const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError, { locale: userLocale });
569
+ const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus], { locale: userLocale });
641
570
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
642
571
  });
643
572
  bot.command('version', async ctx => {
@@ -754,7 +683,7 @@ async function handleSolveCommand(ctx) {
754
683
  let userArgs = parseCommandArgs(ctx.message.text);
755
684
 
756
685
  // Issue #594: strip --show-limits from userArgs (hive-telegram-bot virtual option).
757
- const solveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED });
686
+ const solveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED, locale: solveLocale });
758
687
  if (solveSL.handled) return;
759
688
  const solveShowLimits = solveSL.showLimits;
760
689
  userArgs = solveSL.args;
@@ -812,13 +741,13 @@ async function handleSolveCommand(ctx) {
812
741
  return;
813
742
  }
814
743
 
815
- const validation = await validateGitHubUrl(userArgs, { createYargsConfig: createSolveYargsConfig, positionalNames: ['issue-url'] });
744
+ const validation = await validateGitHubUrl(userArgs, { createYargsConfig: createSolveYargsConfig, positionalNames: ['issue-url'], locale: solveLocale });
816
745
  if (!validation.valid) {
817
746
  let errorMsg = `āŒ ${validation.error}`;
818
747
  if (validation.suggestion) {
819
- errorMsg += `\n\nšŸ’” Did you mean: \`${validation.suggestion}\``;
748
+ errorMsg += `\n\n${t('telegram.did_you_mean', { suggestion: validation.suggestion }, { locale: solveLocale })}`;
820
749
  }
821
- errorMsg += '\n\nExample: `/solve https://github.com/owner/repo/issues/123`\n\nOr reply to a message containing a GitHub link with `/solve`';
750
+ errorMsg += `\n\n${t('telegram.solve_invalid_url_help', {}, { locale: solveLocale })}`;
822
751
  await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
823
752
  return;
824
753
  }
@@ -896,10 +825,14 @@ async function handleSolveCommand(ctx) {
896
825
  const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
897
826
  // #1228: only user options; #1460: escape; #1688: 'Issue:' / 'Pull request:' label so completion can append PR link.
898
827
  const userOptionsRaw = userArgs.slice(1).join(' ');
899
- const urlLabel = validation.parsed?.type === 'pull' ? 'Pull request' : 'Issue';
900
- let infoBlock = `Requested by: ${requester}\n${urlLabel}: ${escapeMarkdown(normalizedUrl)}`;
901
- if (userOptionsRaw) infoBlock += `\n\nšŸ›  Options: ${escapeMarkdown(userOptionsRaw)}`;
902
- if (solveOverrides.length > 0) infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}šŸ”’ Locked options: ${escapeMarkdown(solveOverrides.join(' '))}`;
828
+ let infoBlock = buildTelegramInfoBlock({
829
+ locale: solveLocale,
830
+ requester,
831
+ urlKind: validation.parsed?.type === 'pull' ? 'pullRequest' : 'issue',
832
+ url: escapeMarkdown(normalizedUrl),
833
+ optionsRaw: userOptionsRaw ? escapeMarkdown(userOptionsRaw) : '',
834
+ lockedOptions: solveOverrides.length > 0 ? escapeMarkdown(solveOverrides.join(' ')) : '',
835
+ });
903
836
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
904
837
 
905
838
  // Check for duplicate URL in queue (issue #1080)
@@ -932,15 +865,14 @@ async function handleSolveCommand(ctx) {
932
865
 
933
866
  // Issue #594: append "Limits at start" to infoBlock; thread snapshot via sessionInfo.
934
867
  let solveLimitsAtStart = null;
935
- if (solveShowLimits) ({ infoBlock, limitsAtStart: solveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: solveTool, verbose: VERBOSE, limitsLib, commandLabel: '/solve' }));
868
+ if (solveShowLimits) ({ infoBlock, limitsAtStart: solveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: solveTool, verbose: VERBOSE, limitsLib, commandLabel: '/solve', locale: solveLocale }));
936
869
 
937
870
  if (check.canStart && toolQueuedCount === 0) {
938
- const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
939
- await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart });
871
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock, locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
872
+ await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
940
873
  } else {
941
- const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart });
942
- let queueMessage = `šŸ“‹ Solve command queued (${solveTool} queue position #${toolQueuedCount + 1})\n\n${infoBlock}`; // tool-specific position (#1551)
943
- if (check.reason) queueMessage += `\n\nā³ Waiting: ${escapeMarkdown(check.reason)}`;
874
+ const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale });
875
+ const queueMessage = buildSolveQueuedMessage({ locale: solveLocale, tool: solveTool, position: toolQueuedCount + 1, infoBlock, reason: check.reason ? escapeMarkdown(check.reason) : '' }); // tool-specific position (#1551)
944
876
  const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
945
877
  queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
946
878
  if (!solveQueue.executeCallback) {
@@ -1028,17 +960,17 @@ async function handleHiveCommand(ctx) {
1028
960
  let userArgs = parseCommandArgs(ctx.message.text);
1029
961
 
1030
962
  // Issue #594: see /solve handler.
1031
- const hiveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED });
963
+ const hiveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED, locale: hiveLocale });
1032
964
  if (hiveSL.handled) return;
1033
965
  const hiveShowLimits = hiveSL.showLimits;
1034
966
  userArgs = hiveSL.args;
1035
967
 
1036
968
  // Issue #1102: Allow issues_list/pulls_list URLs and normalize to repo URLs
1037
- const validation = await validateGitHubUrl(userArgs, { allowedTypes: ['repo', 'organization', 'user', 'issues_list', 'pulls_list'], commandName: 'hive', createYargsConfig: createHiveYargsConfig, positionalNames: ['github-url'] });
969
+ const validation = await validateGitHubUrl(userArgs, { allowedTypes: ['repo', 'organization', 'user', 'issues_list', 'pulls_list'], commandName: 'hive', createYargsConfig: createHiveYargsConfig, positionalNames: ['github-url'], locale: hiveLocale });
1038
970
  if (!validation.valid) {
1039
971
  let errorMsg = `āŒ ${validation.error}`;
1040
- if (validation.suggestion) errorMsg += `\n\nšŸ’” Did you mean: \`${escapeMarkdown(validation.suggestion)}\``;
1041
- errorMsg += '\n\nExample: `/hive https://github.com/owner/repo`';
972
+ if (validation.suggestion) errorMsg += `\n\n${t('telegram.did_you_mean', { suggestion: escapeMarkdown(validation.suggestion) }, { locale: hiveLocale })}`;
973
+ errorMsg += `\n\n${t('telegram.hive_invalid_url_help', {}, { locale: hiveLocale })}`;
1042
974
  await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
1043
975
  return;
1044
976
  }
@@ -1090,31 +1022,33 @@ async function handleHiveCommand(ctx) {
1090
1022
  try {
1091
1023
  await parseArgsWithYargs(args, yargs, createHiveYargsConfig);
1092
1024
  } catch (error) {
1093
- await safeReply(ctx, `āŒ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
1025
+ await safeReply(ctx, t('telegram.invalid_options', { message: escapeMarkdown(error.message || String(error)) }, { locale: hiveLocale }), {
1094
1026
  reply_to_message_id: ctx.message.message_id,
1095
1027
  });
1096
1028
  return;
1097
1029
  }
1098
1030
 
1099
1031
  const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
1100
- const escapedUrl = escapeMarkdown(args[0]);
1101
1032
  // Issue #1228: Show only user-provided options (exclude locked overrides to avoid duplication)
1102
1033
  // Issue #1460: Escape options text to prevent Markdown parsing errors
1103
1034
  const userOptionsRaw = normalizedArgs.slice(1).join(' ');
1104
- let infoBlock = `Requested by: ${requester}\nURL: ${escapedUrl}`;
1105
- if (userOptionsRaw) infoBlock += `\n\nšŸ›  Options: ${escapeMarkdown(userOptionsRaw)}`;
1106
- if (hiveOverrides.length > 0) {
1107
- infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}šŸ”’ Locked options: ${escapeMarkdown(hiveOverrides.join(' '))}`;
1108
- }
1035
+ let infoBlock = buildTelegramInfoBlock({
1036
+ locale: hiveLocale,
1037
+ requester,
1038
+ urlKind: 'url',
1039
+ url: escapeMarkdown(args[0]),
1040
+ optionsRaw: userOptionsRaw ? escapeMarkdown(userOptionsRaw) : '',
1041
+ lockedOptions: hiveOverrides.length > 0 ? escapeMarkdown(hiveOverrides.join(' ')) : '',
1042
+ });
1109
1043
 
1110
1044
  // Issue #594: see /solve handler.
1111
1045
  let hiveLimitsAtStart = null;
1112
- if (hiveShowLimits) ({ infoBlock, limitsAtStart: hiveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: hiveTool, verbose: VERBOSE, limitsLib, commandLabel: '/hive' }));
1046
+ if (hiveShowLimits) ({ infoBlock, limitsAtStart: hiveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: hiveTool, verbose: VERBOSE, limitsLib, commandLabel: '/hive', locale: hiveLocale }));
1113
1047
 
1114
- const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
1048
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock, locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1115
1049
  // Issue #378: propagate user's effective Telegram locale to the spawned hive session.
1116
1050
  const hiveArgsWithLocale = injectLanguageIfMissing(args, hiveLocale);
1117
- await executeAndUpdateMessage(ctx, startingMessage, 'hive', hiveArgsWithLocale, infoBlock, effectiveHiveIsolation, hiveTool, null, { showLimits: hiveShowLimits, limitsAtStart: hiveLimitsAtStart });
1051
+ await executeAndUpdateMessage(ctx, startingMessage, 'hive', hiveArgsWithLocale, infoBlock, effectiveHiveIsolation, hiveTool, null, { showLimits: hiveShowLimits, limitsAtStart: hiveLimitsAtStart, locale: hiveLocale });
1118
1052
  }
1119
1053
 
1120
1054
  bot.command(/^hive$/i, handleHiveCommand);
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { exec as execCallback } from 'child_process';
4
+ import { t } from './i18n.lib.mjs';
4
5
 
5
6
  const exec = promisify(execCallback);
6
7
 
@@ -98,7 +99,7 @@ function executeWithCommand(startScreenCmd, command, args, verbose = false) {
98
99
  */
99
100
  export function buildExecuteAndUpdateMessage(deps) {
100
101
  const { resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen, trackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot, formatExecutingWorkSessionMessage } = deps;
101
- return async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude', urlContext = null, { showLimits = false, limitsAtStart = null } = {}) {
102
+ return async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude', urlContext = null, { showLimits = false, limitsAtStart = null, locale = null } = {}) {
102
103
  const { chat, message_id: msgId } = startingMessage;
103
104
  const safeEdit = async text => {
104
105
  try {
@@ -108,7 +109,7 @@ export function buildExecuteAndUpdateMessage(deps) {
108
109
  }
109
110
  };
110
111
  const requesterUserId = ctx.from?.id ?? null; // Issue #1688: suppress duplicate /subscribe DM
111
- const baseSessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock, urlContext, requesterUserId, showLimits, limitsAtStart }; // #594: showLimits/limitsAtStart
112
+ const baseSessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock, urlContext, requesterUserId, showLimits, limitsAtStart, locale }; // #594: showLimits/limitsAtStart
112
113
  const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
113
114
  let result, session, sessionInfo;
114
115
  if (iso) {
@@ -131,9 +132,9 @@ export function buildExecuteAndUpdateMessage(deps) {
131
132
  }
132
133
  if (result.warning) return safeEdit(`āš ļø ${result.warning}`);
133
134
  if (result.success) {
134
- await safeEdit(formatExecutingWorkSessionMessage({ sessionName: session, isolationBackend: iso?.backend || null, infoBlock }));
135
+ await safeEdit(formatExecutingWorkSessionMessage({ sessionName: session, isolationBackend: iso?.backend || null, infoBlock, locale }));
135
136
  if (AUTO_WATCH_MESSAGE && commandName === 'solve' && sessionInfo?.isolationBackend) await startAutoTerminalWatchForSession({ bot, ctx, sessionId: session, sessionInfo, verbose: VERBOSE });
136
- } else await safeEdit(`āŒ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${infoBlock}`);
137
+ } else await safeEdit(`${t('telegram.error_executing_command', { commandName }, { locale })}:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${infoBlock}`);
137
138
  };
138
139
  }
139
140
 
@@ -20,6 +20,8 @@
20
20
  * @see https://github.com/link-assistant/hive-mind/issues/594
21
21
  */
22
22
 
23
+ import { lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
24
+
23
25
  const SHOW_LIMITS_FLAG = '--show-limits';
24
26
  const NO_SHOW_LIMITS_FLAG = '--no-show-limits';
25
27
 
@@ -106,9 +108,9 @@ function pct(value) {
106
108
  return Math.floor(num);
107
109
  }
108
110
 
109
- function formatPercentage(value) {
111
+ function formatPercentage(value, locale = null) {
110
112
  const p = pct(value);
111
- return p === null ? 'N/A' : `${p}%`;
113
+ return p === null ? lt('na', {}, { locale }) : `${p}%`;
112
114
  }
113
115
 
114
116
  /**
@@ -118,26 +120,28 @@ function formatPercentage(value) {
118
120
  * @param {Object} snapshot - Result of captureLimitsSnapshot for tool=claude
119
121
  * @returns {string}
120
122
  */
121
- function formatClaudeSnapshotCompact(snapshot) {
122
- if (!snapshot) return 'Claude limits: N/A';
123
- if (!snapshot.success) return `Claude limits: ${snapshot.error || 'unavailable'}`;
123
+ function formatClaudeSnapshotCompact(snapshot, options = {}) {
124
+ const locale = resolveLimitLocale(options);
125
+ if (!snapshot) return `${lt('claude_limits', {}, { locale })}: ${lt('na', {}, { locale })}`;
126
+ if (!snapshot.success) return `${lt('claude_limits', {}, { locale })}: ${snapshot.error || lt('unavailable', {}, { locale })}`;
124
127
  const usage = snapshot.usage || {};
125
128
  const lines = [];
126
- lines.push(`5h session: ${formatPercentage(usage.currentSession?.percentage)}`);
127
- lines.push(`7d all models: ${formatPercentage(usage.allModels?.percentage)}`);
129
+ lines.push(`${lt('five_hour_session', {}, { locale })}: ${formatPercentage(usage.currentSession?.percentage, locale)}`);
130
+ lines.push(`${lt('seven_day_all_models', {}, { locale })}: ${formatPercentage(usage.allModels?.percentage, locale)}`);
128
131
  if (usage.sonnetOnly && usage.sonnetOnly.percentage !== null && usage.sonnetOnly.percentage !== undefined) {
129
- lines.push(`7d Sonnet only: ${formatPercentage(usage.sonnetOnly.percentage)}`);
132
+ lines.push(`${lt('seven_day_sonnet_only', {}, { locale })}: ${formatPercentage(usage.sonnetOnly.percentage, locale)}`);
130
133
  }
131
134
  return lines.join('\n');
132
135
  }
133
136
 
134
- function formatCodexSnapshotCompact(snapshot) {
135
- if (!snapshot) return 'Codex limits: N/A';
136
- if (!snapshot.success) return `Codex limits: ${snapshot.error || 'unavailable'}`;
137
+ function formatCodexSnapshotCompact(snapshot, options = {}) {
138
+ const locale = resolveLimitLocale(options);
139
+ if (!snapshot) return `${lt('codex_limits', {}, { locale })}: ${lt('na', {}, { locale })}`;
140
+ if (!snapshot.success) return `${lt('codex_limits', {}, { locale })}: ${snapshot.error || lt('unavailable', {}, { locale })}`;
137
141
  const usage = snapshot.usage || {};
138
142
  const lines = [];
139
- lines.push(`5h session: ${formatPercentage(usage.currentSession?.percentage)}`);
140
- lines.push(`Weekly: ${formatPercentage(usage.allModels?.percentage)}`);
143
+ lines.push(`${lt('five_hour_session', {}, { locale })}: ${formatPercentage(usage.currentSession?.percentage, locale)}`);
144
+ lines.push(`${lt('weekly', {}, { locale })}: ${formatPercentage(usage.allModels?.percentage, locale)}`);
141
145
  return lines.join('\n');
142
146
  }
143
147
 
@@ -150,11 +154,12 @@ function formatCodexSnapshotCompact(snapshot) {
150
154
  * @param {string} [options.title='šŸ“Š Limits at start'] Block title
151
155
  * @returns {string}
152
156
  */
153
- export function formatLimitsSnapshotBlock(snapshot, { title = 'šŸ“Š Limits at start' } = {}) {
157
+ export function formatLimitsSnapshotBlock(snapshot, { title = null, locale = null } = {}) {
154
158
  if (!snapshot) return '';
155
159
  const heading = snapshot.toolKey === 'codex' ? 'Codex' : 'Claude';
156
- const body = snapshot.toolKey === 'codex' ? formatCodexSnapshotCompact(snapshot) : formatClaudeSnapshotCompact(snapshot);
157
- return `${title} (${heading})\n\`\`\`\n${body}\n\`\`\``;
160
+ const localizedTitle = title || `šŸ“Š ${lt('limits_at_start', {}, { locale })}`;
161
+ const body = snapshot.toolKey === 'codex' ? formatCodexSnapshotCompact(snapshot, { locale }) : formatClaudeSnapshotCompact(snapshot, { locale });
162
+ return `${localizedTitle} (${heading})\n\`\`\`\n${body}\n\`\`\``;
158
163
  }
159
164
 
160
165
  function deltaFor(startPct, endPct) {
@@ -164,8 +169,8 @@ function deltaFor(startPct, endPct) {
164
169
  return e - s;
165
170
  }
166
171
 
167
- function formatDeltaValue(delta) {
168
- if (delta === null || delta === undefined) return 'N/A';
172
+ function formatDeltaValue(delta, locale = null) {
173
+ if (delta === null || delta === undefined) return lt('na', {}, { locale });
169
174
  if (delta === 0) return '±0%';
170
175
  const sign = delta > 0 ? '+' : '';
171
176
  return `${sign}${delta}%`;
@@ -177,37 +182,39 @@ function formatDeltaValue(delta) {
177
182
  *
178
183
  * @param {Object|null} startSnapshot
179
184
  * @param {Object|null} endSnapshot
185
+ * @param {Object|string} [options]
180
186
  * @returns {string}
181
187
  */
182
- export function formatLimitsDeltaBlock(startSnapshot, endSnapshot) {
188
+ export function formatLimitsDeltaBlock(startSnapshot, endSnapshot, options = {}) {
183
189
  if (!startSnapshot || !endSnapshot) return '';
184
190
  if (startSnapshot.toolKey !== endSnapshot.toolKey) return '';
191
+ const locale = resolveLimitLocale(options);
185
192
  const heading = startSnapshot.toolKey === 'codex' ? 'Codex' : 'Claude';
186
193
  const lines = [];
187
194
 
188
195
  if (!startSnapshot.success && !endSnapshot.success) {
189
- lines.push(`Start: ${startSnapshot.error || 'unavailable'}`);
190
- lines.push(`End: ${endSnapshot.error || 'unavailable'}`);
196
+ lines.push(`${lt('start', {}, { locale })}: ${startSnapshot.error || lt('unavailable', {}, { locale })}`);
197
+ lines.push(`${lt('end', {}, { locale })}: ${endSnapshot.error || lt('unavailable', {}, { locale })}`);
191
198
  } else {
192
199
  const startUsage = startSnapshot.usage || {};
193
200
  const endUsage = endSnapshot.usage || {};
194
201
 
195
- const sessionLabel = '5h session';
196
- lines.push(`${sessionLabel}: ${formatPercentage(startUsage.currentSession?.percentage)} → ${formatPercentage(endUsage.currentSession?.percentage)} (${formatDeltaValue(deltaFor(startUsage.currentSession?.percentage, endUsage.currentSession?.percentage))})`);
202
+ const sessionLabel = lt('five_hour_session', {}, { locale });
203
+ lines.push(`${sessionLabel}: ${formatPercentage(startUsage.currentSession?.percentage, locale)} → ${formatPercentage(endUsage.currentSession?.percentage, locale)} (${formatDeltaValue(deltaFor(startUsage.currentSession?.percentage, endUsage.currentSession?.percentage), locale)})`);
197
204
 
198
- const allModelsLabel = startSnapshot.toolKey === 'codex' ? 'Weekly' : '7d all models';
199
- lines.push(`${allModelsLabel}: ${formatPercentage(startUsage.allModels?.percentage)} → ${formatPercentage(endUsage.allModels?.percentage)} (${formatDeltaValue(deltaFor(startUsage.allModels?.percentage, endUsage.allModels?.percentage))})`);
205
+ const allModelsLabel = startSnapshot.toolKey === 'codex' ? lt('weekly', {}, { locale }) : lt('seven_day_all_models', {}, { locale });
206
+ lines.push(`${allModelsLabel}: ${formatPercentage(startUsage.allModels?.percentage, locale)} → ${formatPercentage(endUsage.allModels?.percentage, locale)} (${formatDeltaValue(deltaFor(startUsage.allModels?.percentage, endUsage.allModels?.percentage), locale)})`);
200
207
 
201
208
  if (startSnapshot.toolKey === 'claude' && ((startUsage.sonnetOnly && startUsage.sonnetOnly.percentage !== null && startUsage.sonnetOnly.percentage !== undefined) || (endUsage.sonnetOnly && endUsage.sonnetOnly.percentage !== null && endUsage.sonnetOnly.percentage !== undefined))) {
202
- lines.push(`7d Sonnet only: ${formatPercentage(startUsage.sonnetOnly?.percentage)} → ${formatPercentage(endUsage.sonnetOnly?.percentage)} (${formatDeltaValue(deltaFor(startUsage.sonnetOnly?.percentage, endUsage.sonnetOnly?.percentage))})`);
209
+ lines.push(`${lt('seven_day_sonnet_only', {}, { locale })}: ${formatPercentage(startUsage.sonnetOnly?.percentage, locale)} → ${formatPercentage(endUsage.sonnetOnly?.percentage, locale)} (${formatDeltaValue(deltaFor(startUsage.sonnetOnly?.percentage, endUsage.sonnetOnly?.percentage), locale)})`);
203
210
  }
204
211
  }
205
212
 
206
213
  // Note: delta is not precise because multiple parallel tasks may consume
207
214
  // from the same Anthropic/OpenAI budget windows during the run.
208
- lines.push('Note: delta is approximate (parallel sessions share the same budget).');
215
+ lines.push(lt('note_delta_approx', {}, { locale }));
209
216
 
210
- return `šŸ“Š Limits change (${heading})\n\`\`\`\n${lines.join('\n')}\n\`\`\``;
217
+ return `šŸ“Š ${lt('limits_change', {}, { locale })} (${heading})\n\`\`\`\n${lines.join('\n')}\n\`\`\``;
211
218
  }
212
219
 
213
220
  /**
@@ -241,10 +248,10 @@ export function appendInfoSection(infoBlock, addition) {
241
248
  * @param {boolean} options.enabled - Master switch (config.showLimits)
242
249
  * @returns {Promise<{ handled: boolean, args: string[], showLimits: boolean }>}
243
250
  */
244
- export async function handleShowLimitsFlag({ ctx, safeReply, args, enabled }) {
251
+ export async function handleShowLimitsFlag({ ctx, safeReply, args, enabled, locale = null }) {
245
252
  const { showLimits, args: stripped } = extractShowLimitsFlag(args);
246
253
  if (showLimits === true && !enabled) {
247
- await safeReply(ctx, 'āŒ `--show-limits` is disabled by the bot administrator.', { reply_to_message_id: ctx.message.message_id });
254
+ await safeReply(ctx, `āŒ ${lt('disabled_by_admin', {}, { locale })}`, { reply_to_message_id: ctx.message.message_id });
248
255
  return { handled: true, args: stripped, showLimits: false };
249
256
  }
250
257
  return { handled: false, args: stripped, showLimits: showLimits === true && enabled };
@@ -264,12 +271,12 @@ export async function handleShowLimitsFlag({ ctx, safeReply, args, enabled }) {
264
271
  * @param {string} [options.commandLabel='command'] - For verbose logging
265
272
  * @returns {Promise<{ infoBlock: string, limitsAtStart: Object|null }>}
266
273
  */
267
- export async function captureStartSnapshotAndAppend({ infoBlock, tool = 'claude', verbose = false, limitsLib, commandLabel = 'command' } = {}) {
274
+ export async function captureStartSnapshotAndAppend({ infoBlock, tool = 'claude', verbose = false, limitsLib, commandLabel = 'command', locale = null } = {}) {
268
275
  let limitsAtStart = null;
269
276
  let nextInfoBlock = infoBlock || '';
270
277
  try {
271
278
  limitsAtStart = await captureLimitsSnapshot({ tool, verbose, limitsLib });
272
- const block = formatLimitsSnapshotBlock(limitsAtStart, { title: 'šŸ“Š Limits at start' });
279
+ const block = formatLimitsSnapshotBlock(limitsAtStart, { locale });
273
280
  if (block) nextInfoBlock = appendInfoSection(nextInfoBlock, block);
274
281
  } catch (e) {
275
282
  if (verbose) console.log(`[VERBOSE] ${commandLabel} --show-limits snapshot failed: ${e?.message || e}`);
@@ -21,6 +21,7 @@ import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunni
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
23
  import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
24
+ import { t } from './i18n.lib.mjs';
24
25
 
25
26
  export const QueueItemStatus = {
26
27
  QUEUED: 'queued',
@@ -51,6 +52,7 @@ class SolveQueueItem {
51
52
  // Issue #594: --show-limits virtual option (hive-telegram-bot only).
52
53
  this.showLimits = options.showLimits === true;
53
54
  this.limitsAtStart = options.limitsAtStart || null;
55
+ this.locale = options.locale || null;
54
56
  this.createdAt = new Date();
55
57
  this.startedAt = null;
56
58
  this.status = QueueItemStatus.QUEUED;
@@ -1094,7 +1096,7 @@ export class SolveQueue {
1094
1096
  this.stats.totalStarted++;
1095
1097
 
1096
1098
  // Update message to show Starting status
1097
- await this.updateItemMessage(item, formatStartingWorkSessionMessage({ infoBlock: item.infoBlock }));
1099
+ await this.updateItemMessage(item, formatStartingWorkSessionMessage({ infoBlock: item.infoBlock, locale: item.locale }));
1098
1100
 
1099
1101
  this.log(`Starting: ${item.toString()} from ${tool} queue`);
1100
1102
 
@@ -1142,7 +1144,7 @@ export class SolveQueue {
1142
1144
 
1143
1145
  if (shouldUpdate) {
1144
1146
  const position = i + 1; // Position within this tool's queue
1145
- await this.updateItemMessage(item, `ā³ Waiting (${tool} queue #${position})\n\n${item.infoBlock}\n\n*Reason:*\n${item.waitingReason}`);
1147
+ await this.updateItemMessage(item, `${t('telegram.solve_waiting', { tool, position }, { locale: item.locale })}\n\n${item.infoBlock}\n\n*${t('telegram.reason_label', {}, { locale: item.locale })}:*\n${item.waitingReason}`);
1146
1148
  }
1147
1149
  }
1148
1150
  }
@@ -1186,10 +1188,11 @@ export class SolveQueue {
1186
1188
  sessionName,
1187
1189
  isolationBackend: result.isolationBackend,
1188
1190
  infoBlock: item.infoBlock,
1191
+ locale: item.locale,
1189
1192
  });
1190
1193
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
1191
1194
  } else {
1192
- const response = `āŒ Error executing solve command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${item.infoBlock}`;
1195
+ const response = `${t('telegram.error_executing_command', { commandName: 'solve' }, { locale: item.locale })}:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${item.infoBlock}`;
1193
1196
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
1194
1197
  }
1195
1198
  } catch (error) {
@@ -1415,6 +1418,7 @@ export function createQueueExecuteCallback(executeStartScreen, trackSessionFn) {
1415
1418
  // handler so it can render an end-of-task delta.
1416
1419
  showLimits: item.showLimits === true,
1417
1420
  limitsAtStart: item.limitsAtStart || null,
1421
+ locale: item.locale || null,
1418
1422
  });
1419
1423
  }
1420
1424
  }