@link-assistant/hive-mind 1.31.4 → 1.32.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.32.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b2c94db: Support all options via /solve command when replying to a message containing a GitHub link (issue #1325)
8
+
9
+ Previously, `/solve` as a reply only worked when used without any arguments. Now users can reply to a message containing a GitHub issue/PR link with `/solve --model opus` or any other options, and the bot will:
10
+ 1. Extract the GitHub URL from the replied message
11
+ 2. Use the provided options
12
+ 3. Execute the solve command with both the extracted URL and the user-provided options
13
+
3
14
  ## 1.31.4
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.31.4",
3
+ "version": "1.32.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -516,9 +516,7 @@ export async function checkMergePermissions(owner, repo, verbose = false) {
516
516
  * @param {string} repo - Repository name
517
517
  * @param {number} prNumber - Pull request number
518
518
  * @param {Object} options - Merge options
519
- * @param {string} options.mergeMethod - Merge method: 'merge', 'squash', or 'rebase' (default: 'merge')
520
- * Note: Must specify one method when running non-interactively.
521
- * See Issue #1269 for details.
519
+ * @param {string} options.mergeMethod - Merge method: 'merge', 'squash', or 'rebase' (default: 'merge'). Must specify one method non-interactively (Issue #1269).
522
520
  * @param {boolean} options.squash - DEPRECATED: Use mergeMethod: 'squash' instead
523
521
  * @param {boolean} options.deleteAfter - Whether to delete branch after merge (default: false)
524
522
  * @param {boolean} verbose - Whether to log verbose output
package/src/solve.mjs CHANGED
@@ -98,12 +98,10 @@ const { validateAndExitOnInvalidModel } = modelValidation;
98
98
  const acceptInviteLib = await import('./solve.accept-invite.lib.mjs');
99
99
  const { autoAcceptInviteForRepo } = acceptInviteLib;
100
100
 
101
- // Initialize log file EARLY to capture all output including version and command
102
- // Use default directory (cwd) initially, will be set from argv.logDir after parsing
101
+ // Initialize log file EARLY (use cwd initially, will be updated after argv parsing)
103
102
  const logFile = await initializeLogFile(null);
104
103
 
105
- // Log version and raw command IMMEDIATELY after log file initialization
106
- // This ensures they appear in both console and log file, even if argument parsing fails
104
+ // Log version and raw command IMMEDIATELY after log file initialization (ensures they appear even if parsing fails)
107
105
  const versionInfo = await getVersionInfo();
108
106
  await log('');
109
107
  await log(`🚀 solve v${versionInfo}`);
@@ -221,9 +219,7 @@ if (!(await validateContinueOnlyOnFeedback(argv, isPrUrl, isIssueUrl))) {
221
219
  const tool = argv.tool || 'claude';
222
220
  await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
223
221
 
224
- // Perform all system checks using validation module
225
- // Skip tool CONNECTION validation in dry-run mode or when --skip-tool-connection-check or --no-tool-connection-check is enabled
226
- // Note: This does NOT skip model validation which is performed above
222
+ // Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
227
223
  const skipToolConnectionCheck = argv.dryRun || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
228
224
  if (!(await performSystemChecks(argv.minDiskSpace || 2048, skipToolConnectionCheck, argv.model, argv))) {
229
225
  await safeExit(1, 'System checks failed');
@@ -236,9 +232,7 @@ if (argv.verbose) {
236
232
  await log(` Is PR URL: ${!!isPrUrl}`, { verbose: true });
237
233
  }
238
234
  const claudePath = argv.executeToolWithBun ? 'bunx claude' : process.env.CLAUDE_PATH || 'claude';
239
- // Note: owner, repo, and urlNumber are already extracted from validateGitHubUrl() above
240
- // The parseUrlComponents() call was removed as it had a bug with hash fragments (#issuecomment-xyz)
241
- // and the validation result already provides these values correctly parsed
235
+ // Note: owner, repo, and urlNumber are extracted from validateGitHubUrl() above (parseUrlComponents() removed due to hash fragment bug)
242
236
 
243
237
  // Handle --auto-fork option: automatically fork public repositories without write access
244
238
  if (argv.autoFork && !argv.fork) {
@@ -52,7 +52,7 @@ const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mj
52
52
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
53
53
  const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
54
54
  const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
55
- const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText } = await import('./telegram-message-filters.lib.mjs');
55
+ const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
56
56
  // Import bot launcher with exponential backoff retry (issue #1240)
57
57
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
58
58
 
@@ -313,10 +313,6 @@ function isOldMessage(ctx) {
313
313
  return _isOldMessage(ctx, BOT_START_TIME, { verbose: VERBOSE });
314
314
  }
315
315
 
316
- function isGroupChat(ctx) {
317
- return _isGroupChat(ctx);
318
- }
319
-
320
316
  function isForwardedOrReply(ctx) {
321
317
  return _isForwardedOrReply(ctx, { verbose: VERBOSE });
322
318
  }
@@ -596,46 +592,6 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
596
592
  }
597
593
  }
598
594
 
599
- /**
600
- * Extract GitHub issue/PR URL from message text
601
- * Validates that message contains exactly one GitHub issue/PR link
602
- *
603
- * @param {string} text - Message text to search
604
- * @returns {{ url: string|null, error: string|null, linkCount: number }}
605
- */
606
- function extractGitHubUrl(text) {
607
- if (!text || typeof text !== 'string') {
608
- return { url: null, error: null, linkCount: 0 };
609
- }
610
-
611
- text = cleanNonPrintableChars(text); // Clean non-printable chars before processing
612
- const words = text.split(/\s+/);
613
- const foundUrls = [];
614
-
615
- for (const word of words) {
616
- // Try to parse as GitHub URL
617
- const parsed = parseGitHubUrl(word);
618
-
619
- // Accept issue or PR URLs
620
- if (parsed.valid && (parsed.type === 'issue' || parsed.type === 'pull')) {
621
- foundUrls.push(parsed.normalized);
622
- }
623
- }
624
-
625
- // Check if multiple links were found
626
- if (foundUrls.length === 0) {
627
- return { url: null, error: null, linkCount: 0 };
628
- } else if (foundUrls.length === 1) {
629
- return { url: foundUrls[0], error: null, linkCount: 1 };
630
- } else {
631
- return {
632
- url: null,
633
- error: `Found ${foundUrls.length} GitHub links in the message. Please reply to a message with only one GitHub issue or PR link.`,
634
- linkCount: foundUrls.length,
635
- };
636
- }
637
- }
638
-
639
595
  bot.command('help', async ctx => {
640
596
  if (VERBOSE) {
641
597
  console.log('[VERBOSE] /help command received');
@@ -760,7 +716,7 @@ bot.command('limits', async ctx => {
760
716
  return;
761
717
  }
762
718
 
763
- if (!isGroupChat(ctx)) {
719
+ if (!_isGroupChat(ctx)) {
764
720
  if (VERBOSE) {
765
721
  console.log('[VERBOSE] /limits ignored: not a group chat');
766
722
  }
@@ -809,7 +765,7 @@ bot.command('version', async ctx => {
809
765
  data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
810
766
  });
811
767
  if (isOldMessage(ctx) || isForwardedOrReply(ctx)) return;
812
- 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 });
768
+ 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 });
813
769
  const chatId = ctx.chat.id;
814
770
  if (!isChatAuthorized(chatId)) return await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
815
771
  const fetchingMessage = await ctx.reply('🔄 Gathering version information...', {
@@ -827,7 +783,7 @@ registerAcceptInvitesCommand(bot, {
827
783
  VERBOSE,
828
784
  isOldMessage,
829
785
  isForwardedOrReply,
830
- isGroupChat,
786
+ isGroupChat: _isGroupChat,
831
787
  isChatAuthorized,
832
788
  addBreadcrumb,
833
789
  });
@@ -838,7 +794,7 @@ registerMergeCommand(bot, {
838
794
  VERBOSE,
839
795
  isOldMessage,
840
796
  isForwardedOrReply,
841
- isGroupChat,
797
+ isGroupChat: _isGroupChat,
842
798
  isChatAuthorized,
843
799
  addBreadcrumb,
844
800
  });
@@ -849,7 +805,7 @@ const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, {
849
805
  VERBOSE,
850
806
  isOldMessage,
851
807
  isForwardedOrReply,
852
- isGroupChat,
808
+ isGroupChat: _isGroupChat,
853
809
  isChatAuthorized,
854
810
  addBreadcrumb,
855
811
  getSolveQueue,
@@ -903,7 +859,7 @@ async function handleSolveCommand(ctx) {
903
859
  return;
904
860
  }
905
861
 
906
- if (!isGroupChat(ctx)) {
862
+ if (!_isGroupChat(ctx)) {
907
863
  if (VERBOSE) {
908
864
  console.log('[VERBOSE] /solve ignored: not a group chat');
909
865
  }
@@ -926,17 +882,23 @@ async function handleSolveCommand(ctx) {
926
882
 
927
883
  let userArgs = parseCommandArgs(ctx.message.text);
928
884
 
929
- // Check if this is a reply to a message and user didn't provide URL
885
+ // Check if this is a reply to a message and user didn't provide URL as first argument
930
886
  // In that case, try to extract GitHub URL from the replied message
887
+ // Issue #1325: Support all options via /solve command when replying (e.g., "/solve --model opus")
931
888
  const isReply = message.reply_to_message && message.reply_to_message.message_id && !message.reply_to_message.forum_topic_created;
932
889
 
933
- if (isReply && userArgs.length === 0) {
890
+ // Check if the first argument looks like a GitHub URL
891
+ // If not, we should try to extract the URL from the replied message
892
+ const firstArgIsUrl = userArgs.length > 0 && (userArgs[0].includes('github.com') || userArgs[0].match(/^https?:\/\//));
893
+
894
+ if (isReply && !firstArgIsUrl) {
934
895
  if (VERBOSE) {
935
- console.log('[VERBOSE] /solve is a reply without URL, extracting from replied message...');
896
+ console.log('[VERBOSE] /solve is a reply without URL in args, extracting from replied message...');
897
+ console.log('[VERBOSE] User args:', userArgs);
936
898
  }
937
899
 
938
900
  const replyText = message.reply_to_message.text || '';
939
- const extraction = extractGitHubUrl(replyText);
901
+ const extraction = _extractGitHubUrl(replyText, { parseGitHubUrl, cleanNonPrintableChars });
940
902
 
941
903
  if (extraction.error) {
942
904
  // Multiple links found
@@ -949,18 +911,18 @@ async function handleSolveCommand(ctx) {
949
911
  });
950
912
  return;
951
913
  } else if (extraction.url) {
952
- // Single link found
914
+ // Single link found - prepend it to existing user args (issue #1325)
953
915
  if (VERBOSE) {
954
916
  console.log('[VERBOSE] Extracted URL from reply:', extraction.url);
955
917
  }
956
- // Add the extracted URL as the first argument
957
- userArgs = [extraction.url];
918
+ // Prepend the extracted URL to user's options (e.g., ['--model', 'opus'] -> ['url', '--model', 'opus'])
919
+ userArgs = [extraction.url, ...userArgs];
958
920
  } else {
959
921
  // No link found
960
922
  if (VERBOSE) {
961
923
  console.log('[VERBOSE] No GitHub URL found in replied message');
962
924
  }
963
- await ctx.reply('❌ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`', { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
925
+ await ctx.reply('❌ 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`', { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
964
926
  return;
965
927
  }
966
928
  }
@@ -1113,7 +1075,7 @@ async function handleHiveCommand(ctx) {
1113
1075
  return;
1114
1076
  }
1115
1077
 
1116
- if (!isGroupChat(ctx)) {
1078
+ if (!_isGroupChat(ctx)) {
1117
1079
  if (VERBOSE) {
1118
1080
  console.log('[VERBOSE] /hive ignored: not a group chat');
1119
1081
  }
@@ -1217,7 +1179,7 @@ registerTopCommand(bot, {
1217
1179
  VERBOSE,
1218
1180
  isOldMessage,
1219
1181
  isForwardedOrReply,
1220
- isGroupChat,
1182
+ isGroupChat: _isGroupChat,
1221
1183
  isChatAuthorized,
1222
1184
  });
1223
1185
 
@@ -171,3 +171,48 @@ export function extractCommandFromText(text, botUsername = null) {
171
171
 
172
172
  return { command, botMention };
173
173
  }
174
+
175
+ /**
176
+ * Extract GitHub issue/PR URL from message text.
177
+ * Validates that message contains exactly one GitHub issue/PR link.
178
+ * Extracted from telegram-bot.mjs to reduce file size (issue #1325).
179
+ *
180
+ * @param {string} text - Message text to search
181
+ * @param {Object} deps - Dependencies for parsing
182
+ * @param {Function} deps.parseGitHubUrl - Function to parse GitHub URLs
183
+ * @param {Function} deps.cleanNonPrintableChars - Function to clean non-printable characters
184
+ * @returns {{ url: string|null, error: string|null, linkCount: number }}
185
+ * @see https://github.com/link-assistant/hive-mind/issues/1325
186
+ */
187
+ export function extractGitHubUrl(text, { parseGitHubUrl, cleanNonPrintableChars }) {
188
+ if (!text || typeof text !== 'string') {
189
+ return { url: null, error: null, linkCount: 0 };
190
+ }
191
+
192
+ text = cleanNonPrintableChars(text); // Clean non-printable chars before processing
193
+ const words = text.split(/\s+/);
194
+ const foundUrls = [];
195
+
196
+ for (const word of words) {
197
+ // Try to parse as GitHub URL
198
+ const parsed = parseGitHubUrl(word);
199
+
200
+ // Accept issue or PR URLs
201
+ if (parsed.valid && (parsed.type === 'issue' || parsed.type === 'pull')) {
202
+ foundUrls.push(parsed.normalized);
203
+ }
204
+ }
205
+
206
+ // Check if multiple links were found
207
+ if (foundUrls.length === 0) {
208
+ return { url: null, error: null, linkCount: 0 };
209
+ } else if (foundUrls.length === 1) {
210
+ return { url: foundUrls[0], error: null, linkCount: 1 };
211
+ } else {
212
+ return {
213
+ url: null,
214
+ error: `Found ${foundUrls.length} GitHub links in the message. Please reply to a message with only one GitHub issue or PR link.`,
215
+ linkCount: foundUrls.length,
216
+ };
217
+ }
218
+ }