@link-assistant/hive-mind 1.65.1 → 1.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/task.mjs CHANGED
@@ -50,6 +50,10 @@ try {
50
50
  process.exit(1);
51
51
  }
52
52
 
53
+ // Initialize i18n based on --language (or detected system locale)
54
+ const { initI18n } = await import('./i18n.lib.mjs');
55
+ await initI18n(argv.language);
56
+
53
57
  const taskInput = argv['task-input'] || argv.taskInput || argv._[0];
54
58
  const selectedModel = argv.model || getDefaultTaskModel(argv.tool);
55
59
  const modelValidation = validateModelName(selectedModel, argv.tool);
@@ -297,6 +297,12 @@ await initializeSentry({
297
297
  environment: process.env.NODE_ENV || 'production',
298
298
  });
299
299
 
300
+ // Initialize i18n: pre-load every supported locale so per-user translations
301
+ // can resolve synchronously from the cache when handling Telegram updates.
302
+ const { initI18n, t, preloadAllLocales, resolveLocaleFromTelegramCtx } = await import('./i18n.lib.mjs');
303
+ await initI18n();
304
+ await preloadAllLocales();
305
+
300
306
  const telegrafModule = await use('telegraf');
301
307
  const { Telegraf } = telegrafModule;
302
308
 
@@ -564,6 +570,7 @@ bot.command('help', async ctx => {
564
570
  message += '`/solve_queue` - Show solve queue status\n';
565
571
  message += '*/limits* - Show usage limits\n';
566
572
  message += '*/version* - Show bot and runtime versions\n';
573
+ message += '*/language* `[en|ru|zh|hi]` - Set or show your preferred reply language (in-memory only, per-user)\n';
567
574
  message += '`/accept_invites` - Accept all pending GitHub invitations\n';
568
575
  message += '*/merge* - Merge queue (experimental)\n';
569
576
  message += 'Usage: `/merge <github-repo-url>`\n';
@@ -621,11 +628,12 @@ bot.command('limits', async ctx => {
621
628
  return;
622
629
  }
623
630
 
631
+ const userLocale = resolveLocaleFromTelegramCtx(ctx);
624
632
  if (!_isGroupChat(ctx)) {
625
633
  if (VERBOSE) {
626
634
  console.log('[VERBOSE] /limits ignored: not a group chat');
627
635
  }
628
- await ctx.reply('āŒ The /limits command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
636
+ await ctx.reply(t('telegram.limits_only_in_groups', {}, { locale: userLocale }), { reply_to_message_id: ctx.message.message_id });
629
637
  return;
630
638
  }
631
639
 
@@ -638,7 +646,7 @@ bot.command('limits', async ctx => {
638
646
  }
639
647
 
640
648
  // Send "fetching" message to indicate work is in progress
641
- const fetchingMessage = await ctx.reply('šŸ”„ Fetching usage limits...', {
649
+ const fetchingMessage = await ctx.reply(t('telegram.fetching_limits', {}, { locale: userLocale }), {
642
650
  reply_to_message_id: ctx.message.message_id,
643
651
  });
644
652
 
@@ -651,7 +659,7 @@ bot.command('limits', async ctx => {
651
659
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
652
660
  const queueStatus = await solveQueue.formatStatus();
653
661
  const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError);
654
- const message = 'šŸ“Š *Usage Limits*\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]);
662
+ 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]);
655
663
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
656
664
  });
657
665
  bot.command('version', async ctx => {
@@ -663,16 +671,20 @@ bot.command('version', async ctx => {
663
671
  data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
664
672
  });
665
673
  if (isOldMessage(ctx) || isForwardedOrReply(ctx)) return;
666
- if (!_isGroupChat(ctx)) return await ctx.reply('āŒ The /version command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
674
+ const versionLocale = resolveLocaleFromTelegramCtx(ctx);
675
+ if (!_isGroupChat(ctx)) return await ctx.reply(t('telegram.version_only_in_groups', {}, { locale: versionLocale }), { reply_to_message_id: ctx.message.message_id });
667
676
  if (!isTopicAuthorized(ctx)) return await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
668
- const fetchingMessage = await ctx.reply('šŸ”„ Gathering version information...', {
677
+ const fetchingMessage = await ctx.reply(t('telegram.gathering_version', {}, { locale: versionLocale }), {
669
678
  reply_to_message_id: ctx.message.message_id,
670
679
  });
671
680
  const result = await getVersionInfo(VERBOSE);
672
681
  if (!result.success) return await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `āŒ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2' });
673
- await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, 'šŸ¤– *Version Information*\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
682
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, t('telegram.version_information_title', {}, { locale: versionLocale }) + '\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
674
683
  });
675
684
 
685
+ const { registerLanguageCommand } = await import('./telegram-language-command.lib.mjs');
686
+ registerLanguageCommand(bot, { VERBOSE, isOldMessage, isForwardedOrReply });
687
+
676
688
  const { registerAcceptInvitesCommand } = await import('./telegram-accept-invitations.lib.mjs');
677
689
  const sharedCommandOpts = { VERBOSE, isOldMessage, isForwardedOrReply, isGroupChat: _isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage };
678
690
  registerAcceptInvitesCommand(bot, sharedCommandOpts);
@@ -704,11 +716,12 @@ async function handleSolveCommand(ctx) {
704
716
  },
705
717
  });
706
718
 
719
+ const solveLocale = resolveLocaleFromTelegramCtx(ctx);
707
720
  if (!solveEnabled) {
708
721
  if (VERBOSE) {
709
722
  console.log(`[VERBOSE] ${solveCommandDisplay} ignored: command disabled`);
710
723
  }
711
- await ctx.reply('āŒ The solve command is disabled on this bot instance.');
724
+ await ctx.reply(t('telegram.solve_disabled', {}, { locale: solveLocale }));
712
725
  return;
713
726
  }
714
727
 
@@ -737,7 +750,7 @@ async function handleSolveCommand(ctx) {
737
750
  if (VERBOSE) {
738
751
  console.log(`[VERBOSE] ${solveCommandDisplay} ignored: not a group chat`);
739
752
  }
740
- await ctx.reply(`āŒ The ${solveCommandDisplay} command only works in group chats. Please add this bot to a group and make it an admin.`, { reply_to_message_id: ctx.message.message_id });
753
+ await ctx.reply(t('telegram.solve_only_in_groups', { commandDisplay: solveCommandDisplay }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
741
754
  return;
742
755
  }
743
756
 
@@ -802,7 +815,7 @@ async function handleSolveCommand(ctx) {
802
815
  if (VERBOSE) {
803
816
  console.log('[VERBOSE] No GitHub URL found in replied message');
804
817
  }
805
- await safeReply(ctx, 'āŒ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`\n\nOr with options: `/solve --model opus`', { reply_to_message_id: ctx.message.message_id });
818
+ await safeReply(ctx, t('telegram.no_github_link_in_reply', {}, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
806
819
  return;
807
820
  }
808
821
  }
@@ -811,7 +824,7 @@ async function handleSolveCommand(ctx) {
811
824
 
812
825
  const { malformed, errors: malformedErrors } = detectMalformedFlags(userArgs);
813
826
  if (malformed.length > 0) {
814
- await safeReply(ctx, `āŒ ${escapeMarkdown(malformedErrors.join('\n'))}\n\nPlease check your option syntax.`, { reply_to_message_id: ctx.message.message_id });
827
+ await safeReply(ctx, `āŒ ${escapeMarkdown(malformedErrors.join('\n'))}\n\n${t('telegram.option_syntax_check', {}, { locale: solveLocale })}`, { reply_to_message_id: ctx.message.message_id });
815
828
  return;
816
829
  }
817
830
 
@@ -828,13 +841,13 @@ async function handleSolveCommand(ctx) {
828
841
  userArgs = moveArgumentToFront(userArgs, validation.normalizedUrl, cleanNonPrintableChars);
829
842
  const { backend: solvePerCommandIsolation, filteredArgs: userArgsWithoutIsolation } = extractIsolationFromArgs(userArgs); // issue #1534
830
843
  if (solvePerCommandIsolation && !isValidPerCommandIsolation(solvePerCommandIsolation)) {
831
- await safeReply(ctx, `āŒ Invalid --isolation value '${escapeMarkdown(solvePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
844
+ await safeReply(ctx, t('telegram.invalid_isolation', { value: escapeMarkdown(solvePerCommandIsolation) }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
832
845
  return;
833
846
  }
834
847
  const mergedSolveArgs = mergeArgsWithOverrides(userArgsWithoutIsolation, solveOverrides);
835
848
  const { backend: solveOverrideIsolation, filteredArgs: args } = extractIsolationFromArgs(mergedSolveArgs);
836
849
  if (solveOverrideIsolation && !isValidPerCommandIsolation(solveOverrideIsolation)) {
837
- await safeReply(ctx, `āŒ Invalid locked --isolation value '${escapeMarkdown(solveOverrideIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
850
+ await safeReply(ctx, t('telegram.invalid_locked_isolation', { value: escapeMarkdown(solveOverrideIsolation) }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
838
851
  return;
839
852
  }
840
853
  const effectiveSolveIsolation = solveOverrideIsolation || solvePerCommandIsolation;
@@ -863,7 +876,7 @@ async function handleSolveCommand(ctx) {
863
876
  }
864
877
  const { malformed: mergedMalformed, errors: mergedMalformedErrors } = detectMalformedFlags(args);
865
878
  if (mergedMalformed.length > 0) {
866
- await safeReply(ctx, `āŒ ${escapeMarkdown(mergedMalformedErrors.join('\n'))}\n\nPlease check your option syntax.`, { reply_to_message_id: ctx.message.message_id });
879
+ await safeReply(ctx, `āŒ ${escapeMarkdown(mergedMalformedErrors.join('\n'))}\n\n${t('telegram.option_syntax_check', {}, { locale: solveLocale })}`, { reply_to_message_id: ctx.message.message_id });
867
880
  return;
868
881
  }
869
882
  // Validate merged arguments using solve's yargs config
@@ -871,7 +884,7 @@ async function handleSolveCommand(ctx) {
871
884
  try {
872
885
  parsedSolveArgs = await parseArgsWithYargs(args, yargs, createSolveYargsConfig);
873
886
  } catch (error) {
874
- await safeReply(ctx, `āŒ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
887
+ await safeReply(ctx, t('telegram.invalid_options', { message: escapeMarkdown(error.message || String(error)) }, { locale: solveLocale }), {
875
888
  reply_to_message_id: ctx.message.message_id,
876
889
  });
877
890
  return;
@@ -909,20 +922,20 @@ async function handleSolveCommand(ctx) {
909
922
  const existingItem = solveQueue.findByUrl(normalizedUrl);
910
923
  if (existingItem) {
911
924
  const statusText = existingItem.status === 'starting' || existingItem.status === 'started' ? 'being processed' : 'already in the queue';
912
- await safeReply(ctx, `āŒ This URL is ${statusText}.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nStatus: ${existingItem.status}\n\nšŸ’” Use /solve_queue to check the queue status.`, { reply_to_message_id: ctx.message.message_id });
925
+ await safeReply(ctx, t('telegram.url_status_active', { statusText, url: escapeMarkdown(normalizedUrl), status: existingItem.status }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
913
926
  return;
914
927
  }
915
928
  // Issue #1567: Prevent concurrent sessions on the same PR/issue
916
929
  const activeSession = await hasActiveSessionForUrlAsync(normalizedUrl, VERBOSE);
917
930
  if (activeSession.isActive) {
918
- await safeReply(ctx, `āŒ A working session is already running for this URL.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nSession: \`${activeSession.sessionName}\`\n\nšŸ’” Wait for the current session to complete, or use /solve\\_stop to cancel it.`, { reply_to_message_id: ctx.message.message_id });
931
+ await safeReply(ctx, t('telegram.url_session_running', { url: escapeMarkdown(normalizedUrl), session: activeSession.sessionName }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
919
932
  return;
920
933
  }
921
934
  const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
922
935
  const queueStats = solveQueue.getStats();
923
936
  // Handle rejection: threshold strategy is 'reject' — fail immediately (issue #1267)
924
937
  if (check.rejected) {
925
- await safeReply(ctx, `āŒ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${escapeMarkdown(check.rejectReason || 'Unknown')}`, { reply_to_message_id: ctx.message.message_id });
938
+ await safeReply(ctx, t('telegram.solve_rejected', { infoBlock, reason: escapeMarkdown(check.rejectReason || 'Unknown') }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
926
939
  return;
927
940
  }
928
941
 
@@ -970,11 +983,12 @@ async function handleHiveCommand(ctx) {
970
983
  },
971
984
  });
972
985
 
986
+ const hiveLocale = resolveLocaleFromTelegramCtx(ctx);
973
987
  if (!hiveEnabled) {
974
988
  if (VERBOSE) {
975
989
  console.log('[VERBOSE] /hive ignored: command disabled');
976
990
  }
977
- await ctx.reply('āŒ The /hive command is disabled on this bot instance.');
991
+ await ctx.reply(t('telegram.hive_disabled', {}, { locale: hiveLocale }));
978
992
  return;
979
993
  }
980
994
 
@@ -998,7 +1012,7 @@ async function handleHiveCommand(ctx) {
998
1012
  if (VERBOSE) {
999
1013
  console.log('[VERBOSE] /hive ignored: not a group chat');
1000
1014
  }
1001
- await ctx.reply('āŒ The /hive command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
1015
+ await ctx.reply(t('telegram.hive_only_in_groups', {}, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1002
1016
  return;
1003
1017
  }
1004
1018
 
@@ -1041,13 +1055,13 @@ async function handleHiveCommand(ctx) {
1041
1055
 
1042
1056
  const { backend: hivePerCommandIsolation, filteredArgs: normalizedArgsWithoutIsolation } = extractIsolationFromArgs(normalizedArgs); // issue #1534
1043
1057
  if (hivePerCommandIsolation && !isValidPerCommandIsolation(hivePerCommandIsolation)) {
1044
- await safeReply(ctx, `āŒ Invalid --isolation value '${escapeMarkdown(hivePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1058
+ await safeReply(ctx, t('telegram.invalid_isolation', { value: escapeMarkdown(hivePerCommandIsolation) }, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1045
1059
  return;
1046
1060
  }
1047
1061
  const mergedHiveArgs = mergeArgsWithOverrides(normalizedArgsWithoutIsolation, hiveOverrides);
1048
1062
  const { backend: hiveOverrideIsolation, filteredArgs: args } = extractIsolationFromArgs(mergedHiveArgs);
1049
1063
  if (hiveOverrideIsolation && !isValidPerCommandIsolation(hiveOverrideIsolation)) {
1050
- await safeReply(ctx, `āŒ Invalid locked --isolation value '${escapeMarkdown(hiveOverrideIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1064
+ await safeReply(ctx, t('telegram.invalid_locked_isolation', { value: escapeMarkdown(hiveOverrideIsolation) }, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1051
1065
  return;
1052
1066
  }
1053
1067
  const effectiveHiveIsolation = hiveOverrideIsolation || hivePerCommandIsolation;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Telegram /language command implementation.
3
+ *
4
+ * Allows each user to override the bot's reply language for the current
5
+ * process. The override is in-memory only (resets when the bot restarts).
6
+ *
7
+ * Usage in chat:
8
+ * /language -> show current language
9
+ * /language <en|ru|zh|hi> -> set language for this user
10
+ * /language default -> clear the override (reset|clear also work)
11
+ */
12
+
13
+ import { t, getSupportedLocales, normalizeLocale, setUserLocale, clearUserLocale, resolveLocaleFromTelegramCtx } from './i18n.lib.mjs';
14
+
15
+ export function registerLanguageCommand(bot, options = {}) {
16
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply } = options;
17
+
18
+ bot.command('language', async ctx => {
19
+ VERBOSE && console.log('[VERBOSE] /language command received');
20
+ if (isOldMessage?.(ctx) || isForwardedOrReply?.(ctx)) return;
21
+ const userId = ctx.from?.id;
22
+ const locale = resolveLocaleFromTelegramCtx(ctx);
23
+ const supported = getSupportedLocales();
24
+ const supportedList = supported.join(', ');
25
+ const text = ctx.message?.text || '';
26
+ const parts = text.trim().split(/\s+/);
27
+ const arg = parts.length > 1 ? parts[1] : null;
28
+ if (!arg) {
29
+ const langName = t(`language.${locale}`, {}, { locale });
30
+ await ctx.reply(t('telegram.language_current', { language: langName, supported: supportedList }, { locale }), { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
31
+ return;
32
+ }
33
+ if (['default', 'reset', 'clear'].includes(arg.toLowerCase())) {
34
+ clearUserLocale(userId);
35
+ const newLocale = resolveLocaleFromTelegramCtx(ctx);
36
+ const langName = t(`language.${newLocale}`, {}, { locale: newLocale });
37
+ await ctx.reply(t('telegram.language_set', { language: langName }, { locale: newLocale }), { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
38
+ return;
39
+ }
40
+ const target = normalizeLocale(arg);
41
+ if (!target) {
42
+ await ctx.reply(t('telegram.language_invalid', { supported: supportedList }, { locale }), { reply_to_message_id: ctx.message.message_id });
43
+ return;
44
+ }
45
+ setUserLocale(userId, target);
46
+ const langName = t(`language.${target}`, {}, { locale: target });
47
+ await ctx.reply(t('telegram.language_set', { language: langName }, { locale: target }), { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
48
+ });
49
+ }