@link-assistant/hive-mind 1.2.5 → 1.2.7

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,26 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.2.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 12831a1: fix: Allow issues_list and pulls_list URLs for /hive command (Issue #1102)
8
+ - Accept issues_list URLs (e.g., `https://github.com/owner/repo/issues`) for /hive command
9
+ - Clean non-printable characters from URLs to prevent Markdown parsing errors
10
+ - Escape special characters in error messages
11
+ - Normalize issues_list URLs to base repo URLs before processing
12
+
13
+ ## 1.2.6
14
+
15
+ ### Patch Changes
16
+
17
+ - 94dfb13: Fix gh-upload-log argument parsing bug causing "File does not exist" error
18
+ - Fixed bug where `gh-upload-log` received all arguments as a single concatenated string
19
+ - The issue was caused by using `${commandArgs.join(' ')}` in command-stream template literal, which treats the entire joined string as one argument
20
+ - Now using separate `${}` interpolations for each argument to ensure proper argument parsing
21
+ - Also fixed: description flag is now properly passed to gh-upload-log (was only displayed, never sent)
22
+ - Added comprehensive regression tests and case study documentation
23
+
3
24
  ## 1.2.5
4
25
 
5
26
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -33,23 +33,30 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
33
33
  const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
34
34
 
35
35
  try {
36
- // Build command with appropriate flags
36
+ // Build command flags
37
+ // IMPORTANT: When using command-stream's $ template tag, each ${} interpolation is treated
38
+ // as a single argument. DO NOT use commandArgs.join(' ') as it will make all flags part
39
+ // of the first positional argument, causing "File does not exist" errors.
40
+ // See case study: docs/case-studies/issue-1096/README.md
37
41
  const publicFlag = isPublic ? '--public' : '--private';
38
- const descFlag = description ? `--description "${description}"` : '';
39
- const verboseFlag = verbose ? '--verbose' : '';
40
-
41
- const command = `gh-upload-log "${logFile}" ${publicFlag} ${descFlag} ${verboseFlag}`.trim().replace(/\s+/g, ' ');
42
42
 
43
43
  if (verbose) {
44
- await log(` šŸ“¤ Running: ${command}`, { verbose: true });
44
+ const descDisplay = description ? ` --description "${description}"` : '';
45
+ await log(` šŸ“¤ Running: gh-upload-log "${logFile}" ${publicFlag}${descDisplay} --verbose`, { verbose: true });
45
46
  }
46
47
 
47
- // Build command arguments array, filtering out empty strings to prevent "Unknown argument: ''" error
48
- const commandArgs = [`"${logFile}"`, publicFlag];
49
- if (verbose) {
50
- commandArgs.push('--verbose');
48
+ // Execute command with separate interpolations for each argument
49
+ // Each ${} is properly passed as a separate argument to the shell
50
+ let uploadResult;
51
+ if (description && verbose) {
52
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --description ${description} --verbose`;
53
+ } else if (description) {
54
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --description ${description}`;
55
+ } else if (verbose) {
56
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --verbose`;
57
+ } else {
58
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag}`;
51
59
  }
52
- const uploadResult = await $`gh-upload-log ${commandArgs.join(' ')}`;
53
60
  const output = (uploadResult.stdout?.toString() || '') + (uploadResult.stderr?.toString() || '');
54
61
 
55
62
  if (uploadResult.code !== 0) {
@@ -595,66 +595,28 @@ function mergeArgsWithOverrides(userArgs, overrides) {
595
595
  return [...filteredArgs, ...overrides];
596
596
  }
597
597
 
598
- /**
599
- * Validate GitHub URL for Telegram bot commands
600
- *
601
- * @param {string[]} args - Command arguments (first arg should be URL)
602
- * @param {Object} options - Validation options
603
- * @param {string[]} options.allowedTypes - Allowed URL types (e.g., ['issue', 'pull'] or ['repository', 'organization', 'user'])
604
- * @param {string} options.commandName - Command name for error messages (e.g., 'solve' or 'hive')
605
- * @param {string} options.exampleUrl - Example URL for error messages
606
- * @returns {{ valid: boolean, error?: string }}
607
- */
598
+ /** Validate GitHub URL for Telegram bot commands. Returns { valid, error?, parsed?, normalizedUrl? } */
608
599
  function validateGitHubUrl(args, options = {}) {
609
- // Default options for /solve command (backward compatibility)
610
600
  const { allowedTypes = ['issue', 'pull'], commandName = 'solve' } = options;
611
-
612
- if (args.length === 0) {
613
- return {
614
- valid: false,
615
- error: `Missing GitHub URL. Usage: /${commandName} <github-url> [options]`,
616
- };
617
- }
618
-
619
- const url = args[0];
620
- if (!url.includes('github.com')) {
621
- return {
622
- valid: false,
623
- error: 'First argument must be a GitHub URL',
624
- };
625
- }
626
-
627
- // Parse the URL to validate structure
601
+ if (args.length === 0) return { valid: false, error: `Missing GitHub URL. Usage: /${commandName} <github-url> [options]` };
602
+ // Issue #1102: Clean non-printable chars (Zero-Width Space, BOM, etc.) from URLs
603
+ const url = cleanNonPrintableChars(args[0]);
604
+ if (!url.includes('github.com')) return { valid: false, error: 'First argument must be a GitHub URL' };
628
605
  const parsed = parseGitHubUrl(url);
629
- if (!parsed.valid) {
630
- return {
631
- valid: false,
632
- error: parsed.error || 'Invalid GitHub URL',
633
- suggestion: parsed.suggestion,
634
- };
635
- }
636
-
637
- // Check if the URL type is allowed for this command
606
+ if (!parsed.valid) return { valid: false, error: parsed.error || 'Invalid GitHub URL', suggestion: parsed.suggestion };
638
607
  if (!allowedTypes.includes(parsed.type)) {
639
608
  const allowedTypesStr = allowedTypes.map(t => (t === 'pull' ? 'pull request' : t)).join(', ');
640
609
  const baseUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
641
-
642
- // Provide specific, helpful error messages based on the URL type
610
+ const escapedUrl = escapeMarkdown(url),
611
+ escapedBaseUrl = escapeMarkdown(baseUrl); // Issue #1102: escape for Markdown
643
612
  let error;
644
- if (parsed.type === 'issues_list') {
645
- error = `URL points to the issues list page, but you need a specific issue\n\nšŸ’” How to fix:\n1. Open the repository: ${url}\n2. Click on a specific issue\n3. Copy the URL (it should end with /issues/NUMBER)\n\nExample: \`${baseUrl}/issues/1\``;
646
- } else if (parsed.type === 'pulls_list') {
647
- 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: ${url}\n2. Click on a specific pull request\n3. Copy the URL (it should end with /pull/NUMBER)\n\nExample: \`${baseUrl}/pull/1\``;
648
- } else if (parsed.type === 'repo') {
649
- error = `URL points to a repository, but you need a specific ${allowedTypesStr}\n\nšŸ’” How to fix:\n1. Go to: ${url}/issues\n2. Click on an issue to solve\n3. Use the full URL with the issue number\n\nExample: \`${baseUrl}/issues/1\``;
650
- } else {
651
- error = `URL must be a GitHub ${allowedTypesStr} (not ${parsed.type.replace('_', ' ')})`;
652
- }
653
-
613
+ 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\``;
614
+ 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\``;
615
+ 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\``;
616
+ else error = `URL must be a GitHub ${allowedTypesStr} (not ${parsed.type.replace('_', ' ')})`;
654
617
  return { valid: false, error };
655
618
  }
656
-
657
- return { valid: true };
619
+ return { valid: true, parsed, normalizedUrl: url };
658
620
  }
659
621
 
660
622
  /**
@@ -1174,23 +1136,25 @@ bot.command(/^hive$/i, async ctx => {
1174
1136
 
1175
1137
  const userArgs = parseCommandArgs(ctx.message.text);
1176
1138
 
1177
- const validation = validateGitHubUrl(userArgs, {
1178
- allowedTypes: ['repo', 'organization', 'user'],
1179
- commandName: 'hive',
1180
- exampleUrl: 'https://github.com/owner/repo',
1181
- });
1139
+ // Issue #1102: Allow issues_list/pulls_list URLs and normalize to repo URLs
1140
+ const validation = validateGitHubUrl(userArgs, { allowedTypes: ['repo', 'organization', 'user', 'issues_list', 'pulls_list'], commandName: 'hive' });
1182
1141
  if (!validation.valid) {
1183
1142
  let errorMsg = `āŒ ${validation.error}`;
1184
- if (validation.suggestion) {
1185
- errorMsg += `\n\nšŸ’” Did you mean: \`${validation.suggestion}\``;
1186
- }
1143
+ if (validation.suggestion) errorMsg += `\n\nšŸ’” Did you mean: \`${escapeMarkdown(validation.suggestion)}\``;
1187
1144
  errorMsg += '\n\nExample: `/hive https://github.com/owner/repo`';
1188
1145
  await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1189
1146
  return;
1190
1147
  }
1148
+ // Normalize issues_list/pulls_list to base repo URL, or use cleaned URL
1149
+ let normalizedArgs = [...userArgs];
1150
+ const p = validation.parsed;
1151
+ if (p && (p.type === 'issues_list' || p.type === 'pulls_list')) {
1152
+ normalizedArgs[0] = `https://github.com/${p.owner}/${p.repo}`;
1153
+ if (VERBOSE) console.log(`[VERBOSE] /hive: Normalized ${p.type} URL to repo URL: ${normalizedArgs[0]}`);
1154
+ } else if (validation.normalizedUrl && validation.normalizedUrl !== userArgs[0]) normalizedArgs[0] = validation.normalizedUrl;
1191
1155
 
1192
1156
  // Merge user args with overrides
1193
- const args = mergeArgsWithOverrides(userArgs, hiveOverrides);
1157
+ const args = mergeArgsWithOverrides(normalizedArgs, hiveOverrides);
1194
1158
 
1195
1159
  // Determine tool from args (default: claude)
1196
1160
  let hiveTool = 'claude';