@link-assistant/hive-mind 0.54.3 → 0.54.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.54.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix duplicate APT sources warning in installation script
8
+ - Add `cleanup_duplicate_apt_sources()` function to detect and remove duplicate APT source files
9
+ - Clean up duplicate Microsoft Edge sources (`microsoft-edge.list` vs `microsoft-edge-stable.list`)
10
+ - Clean up duplicate Google Chrome sources (`google-chrome.list` vs `google-chrome-stable.list`)
11
+ - Run cleanup before `apt update` to prevent "Target Packages configured multiple times" warnings
12
+ - Ensures script supports clean upgrade mode when run on previously installed systems
13
+
14
+ Improve Telegram bot error messages for better user experience (issue #1070)
15
+ - Enhanced URL validation to provide specific, actionable error messages based on URL type (issues list, pulls list, repository)
16
+ - Added step-by-step fix instructions with examples when users provide wrong URL formats
17
+ - Improved global error handler to properly escape Markdown special characters, preventing "400: Bad Request: can't parse entities" errors
18
+ - Added special handling for Telegram API parsing errors with clearer messaging
19
+ - Added `cleanNonPrintableChars()` to automatically remove invisible Unicode characters from user input
20
+ - Added `makeSpecialCharsVisible()` to show users exactly where problematic special characters are in their input
21
+ - Enhanced error messages to display user input with special characters made visible for easier debugging
22
+ - Refactored telegram-bot.mjs to meet 1500 line limit requirement
23
+ - Created comprehensive test suites to verify URL validation improvements and special character handling
24
+ - Documented case study analysis in docs/case-studies/issue-1070/ANALYSIS.md
25
+
26
+ ## 0.54.4
27
+
28
+ ### Patch Changes
29
+
30
+ - 4e53d67: fix: resolve TypeError in telegram-bot when using --tokens-budget-stats
31
+
32
+ Fixed type safety bug that prevented the --tokens-budget-stats option from working via telegram bot configuration overrides. Changed from lino.parse() to lino.parseStringValues() to ensure only string values are returned, making .trim() safe to call. The feature was already fully implemented but crashed when used via TELEGRAM_HIVE_OVERRIDES or TELEGRAM_SOLVE_OVERRIDES.
33
+
3
34
  ## 0.54.3
4
35
 
5
36
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.54.3",
3
+ "version": "0.54.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -47,7 +47,7 @@ const { validateModelName } = await import('./model-validation.lib.mjs');
47
47
  // Import libraries for /limits, /version, and markdown escaping
48
48
  const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
49
49
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
50
- const { escapeMarkdown, escapeMarkdownV2 } = await import('./telegram-markdown.lib.mjs');
50
+ const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
51
51
  const { getSolveQueue, getRunningClaudeProcesses, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
52
52
 
53
53
  const config = yargs(hideBin(process.argv))
@@ -150,7 +150,7 @@ const allowedChats = resolvedAllowedChats ? lino.parseNumericIds(resolvedAllowed
150
150
  const resolvedSolveOverrides = config.solveOverrides || getenv('TELEGRAM_SOLVE_OVERRIDES', '');
151
151
  const solveOverrides = resolvedSolveOverrides
152
152
  ? lino
153
- .parse(resolvedSolveOverrides)
153
+ .parseStringValues(resolvedSolveOverrides)
154
154
  .map(line => line.trim())
155
155
  .filter(line => line)
156
156
  : [];
@@ -158,7 +158,7 @@ const solveOverrides = resolvedSolveOverrides
158
158
  const resolvedHiveOverrides = config.hiveOverrides || getenv('TELEGRAM_HIVE_OVERRIDES', '');
159
159
  const hiveOverrides = resolvedHiveOverrides
160
160
  ? lino
161
- .parse(resolvedHiveOverrides)
161
+ .parseStringValues(resolvedHiveOverrides)
162
162
  .map(line => line.trim())
163
163
  .filter(line => line)
164
164
  : [];
@@ -643,10 +643,21 @@ function validateGitHubUrl(args, options = {}) {
643
643
  // Check if the URL type is allowed for this command
644
644
  if (!allowedTypes.includes(parsed.type)) {
645
645
  const allowedTypesStr = allowedTypes.map(t => (t === 'pull' ? 'pull request' : t)).join(', ');
646
- return {
647
- valid: false,
648
- error: `URL must be a GitHub ${allowedTypesStr} (not ${parsed.type})`,
649
- };
646
+ const baseUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
647
+
648
+ // Provide specific, helpful error messages based on the URL type
649
+ let error;
650
+ if (parsed.type === 'issues_list') {
651
+ 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\``;
652
+ } else if (parsed.type === 'pulls_list') {
653
+ 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\``;
654
+ } else if (parsed.type === 'repo') {
655
+ 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\``;
656
+ } else {
657
+ error = `URL must be a GitHub ${allowedTypesStr} (not ${parsed.type.replace('_', ' ')})`;
658
+ }
659
+
660
+ return { valid: false, error };
650
661
  }
651
662
 
652
663
  return { valid: true };
@@ -707,7 +718,7 @@ function extractGitHubUrl(text) {
707
718
  return { url: null, error: null, linkCount: 0 };
708
719
  }
709
720
 
710
- // Split text into words and check each one
721
+ text = cleanNonPrintableChars(text); // Clean non-printable chars before processing
711
722
  const words = text.split(/\s+/);
712
723
  const foundUrls = [];
713
724
 
@@ -1313,38 +1324,46 @@ bot.catch((error, ctx) => {
1313
1324
 
1314
1325
  // Try to notify the user about the error with more details
1315
1326
  if (ctx?.reply) {
1316
- // Build a more informative error message
1317
- let errorMessage = 'āŒ An error occurred while processing your request.\n\n';
1318
-
1319
- // Add error type/name if available
1320
- if (error.name && error.name !== 'Error') {
1321
- errorMessage += `**Error type:** ${error.name}\n`;
1322
- }
1323
-
1324
- // Add sanitized error message (avoid leaking sensitive info)
1325
- if (error.message) {
1326
- // Filter out potentially sensitive information
1327
- const sanitizedMessage = error.message
1328
- .replace(/token[s]?\s*[:=]\s*[\w-]+/gi, 'token: [REDACTED]')
1329
- .replace(/password[s]?\s*[:=]\s*[\w-]+/gi, 'password: [REDACTED]')
1330
- .replace(/api[_-]?key[s]?\s*[:=]\s*[\w-]+/gi, 'api_key: [REDACTED]');
1331
-
1332
- errorMessage += `**Details:** ${sanitizedMessage}\n`;
1333
- }
1334
-
1335
- errorMessage += '\nšŸ’” **Troubleshooting:**\n';
1336
- errorMessage += '• Try running the command again\n';
1337
- errorMessage += '• Check if all required parameters are correct\n';
1338
- errorMessage += '• If the issue persists, contact support with the error details above\n';
1339
-
1340
- if (VERBOSE) {
1341
- errorMessage += `\nšŸ” **Debug info:** Update ID: ${ctx.update.update_id}`;
1327
+ // Detect if this is a Telegram API parsing error
1328
+ const isTelegramParsingError = error.message && (error.message.includes("can't parse entities") || error.message.includes("Can't parse entities") || error.message.includes("can't find end of") || (error.message.includes('Bad Request') && error.message.includes('400')));
1329
+
1330
+ let errorMessage;
1331
+
1332
+ if (isTelegramParsingError) {
1333
+ // Special handling for Telegram API parsing errors caused by unescaped special characters
1334
+ errorMessage = `āŒ A message formatting error occurred.\n\nšŸ’” This usually means there was a problem with special characters in the response.\nPlease try your command again with a different URL or contact support.`;
1335
+ // Show the user's input with special characters visible (if available)
1336
+ if (ctx.message?.text) {
1337
+ const cleanedInput = cleanNonPrintableChars(ctx.message.text);
1338
+ const visibleInput = makeSpecialCharsVisible(cleanedInput, { maxLength: 150 });
1339
+ if (visibleInput !== cleanedInput) errorMessage += `\n\nšŸ“ Your input (with special chars visible):\n\`${escapeMarkdown(visibleInput)}\``;
1340
+ }
1341
+ if (VERBOSE) {
1342
+ const escapedError = escapeMarkdown(error.message || 'Unknown error');
1343
+ errorMessage += `\n\nšŸ” Debug info: ${escapedError}\nUpdate ID: ${ctx.update.update_id}`;
1344
+ }
1345
+ } else {
1346
+ // Build informative error message for other errors
1347
+ errorMessage = 'āŒ An error occurred while processing your request.\n\n';
1348
+ if (error.message) {
1349
+ // Filter out sensitive info and escape markdown
1350
+ const sanitizedMessage = escapeMarkdown(
1351
+ error.message
1352
+ .replace(/token[s]?\s*[:=]\s*[\w-]+/gi, 'token: [REDACTED]')
1353
+ .replace(/password[s]?\s*[:=]\s*[\w-]+/gi, 'password: [REDACTED]')
1354
+ .replace(/api[_-]?key[s]?\s*[:=]\s*[\w-]+/gi, 'api_key: [REDACTED]')
1355
+ );
1356
+ errorMessage += `Details: ${sanitizedMessage}\n`;
1357
+ }
1358
+ errorMessage += '\nšŸ’” Troubleshooting:\n• Try running the command again\n• Check if all required parameters are correct\n• Use /help to see command examples\n• If the issue persists, contact support with the error details above';
1359
+ if (VERBOSE) errorMessage += `\n\nšŸ” Debug info: Update ID: ${ctx.update.update_id}`;
1342
1360
  }
1343
1361
 
1344
1362
  ctx.reply(errorMessage, { parse_mode: 'Markdown' }).catch(replyError => {
1345
1363
  console.error('Failed to send error message to user:', replyError);
1346
1364
  // Try sending a simple text message without Markdown if Markdown parsing failed
1347
- ctx.reply('āŒ An error occurred while processing your request. Please try again or contact support.').catch(fallbackError => {
1365
+ const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
1366
+ ctx.reply(plainMessage).catch(fallbackError => {
1348
1367
  console.error('Failed to send fallback error message:', fallbackError);
1349
1368
  });
1350
1369
  });
@@ -1361,29 +1380,17 @@ if (allowedChats && allowedChats.length > 0) {
1361
1380
  } else {
1362
1381
  console.log('Allowed chats: All (no restrictions)');
1363
1382
  }
1364
- console.log('Commands enabled:', {
1365
- solve: solveEnabled,
1366
- hive: hiveEnabled,
1367
- });
1368
- if (solveOverrides.length > 0) {
1369
- console.log('Solve overrides (lino):', lino.format(solveOverrides));
1370
- }
1371
- if (hiveOverrides.length > 0) {
1372
- console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1373
- }
1383
+ console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled });
1384
+ if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
1385
+ if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1374
1386
  if (VERBOSE) {
1375
1387
  console.log('[VERBOSE] Verbose logging enabled');
1376
1388
  console.log('[VERBOSE] Bot start time (Unix):', BOT_START_TIME);
1377
1389
  console.log('[VERBOSE] Bot start time (ISO):', new Date(BOT_START_TIME * 1000).toISOString());
1378
1390
  }
1379
1391
 
1380
- // Delete any existing webhook before starting polling
1381
- // This is critical because a webhook prevents polling from working
1382
- // If the bot was previously configured with a webhook (or if one exists),
1383
- // we must delete it to allow polling mode to receive messages
1384
- if (VERBOSE) {
1385
- console.log('[VERBOSE] Deleting webhook...');
1386
- }
1392
+ // Delete existing webhook (critical: webhooks prevent polling from working)
1393
+ if (VERBOSE) console.log('[VERBOSE] Deleting webhook...');
1387
1394
  bot.telegram
1388
1395
  .deleteWebhook({ drop_pending_updates: true })
1389
1396
  .then(result => {
@@ -1398,19 +1405,12 @@ bot.telegram
1398
1405
  });
1399
1406
  }
1400
1407
  return bot.launch({
1401
- // Receive message updates (commands, text messages) and callback queries (button clicks)
1402
- // This ensures the bot receives all message types including commands and button interactions
1403
- allowedUpdates: ['message', 'callback_query'],
1404
- // Drop any pending updates that were sent before the bot started
1405
- // This ensures we only process new messages sent after this bot instance started
1406
- dropPendingUpdates: true,
1408
+ allowedUpdates: ['message', 'callback_query'], // Receive messages and callback queries
1409
+ dropPendingUpdates: true, // Drop pending updates sent before bot started
1407
1410
  });
1408
1411
  })
1409
1412
  .then(async () => {
1410
- // Check if shutdown was initiated before printing success messages
1411
- if (isShuttingDown) {
1412
- return; // Skip success messages if shutting down
1413
- }
1413
+ if (isShuttingDown) return; // Skip success messages if shutting down
1414
1414
 
1415
1415
  console.log('āœ… SwarmMindBot is now running!');
1416
1416
  console.log('Press Ctrl+C to stop');
@@ -62,3 +62,79 @@ export function escapeMarkdownV2(text, options = {}) {
62
62
 
63
63
  return parts.join('');
64
64
  }
65
+
66
+ /**
67
+ * Clean non-printable and problematic Unicode characters from text.
68
+ * Removes zero-width characters, control characters, and other invisible/problematic sequences.
69
+ * @param {string} text - Text to clean
70
+ * @returns {string} Cleaned text
71
+ */
72
+ export function cleanNonPrintableChars(text) {
73
+ if (!text || typeof text !== 'string') return text;
74
+
75
+ return (
76
+ text
77
+ // Remove zero-width characters
78
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
79
+ // Remove other non-printable control characters (except newline, tab, carriage return)
80
+ // eslint-disable-next-line no-control-regex
81
+ .replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F]/g, '')
82
+ // Remove soft hyphens
83
+ .replace(/\u00AD/g, '')
84
+ // Normalize whitespace (replace multiple spaces with single space)
85
+ .replace(/[ \t]+/g, ' ')
86
+ // Trim leading/trailing whitespace from each line
87
+ .split('\n')
88
+ .map(line => line.trim())
89
+ .join('\n')
90
+ .trim()
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Make special characters visible for debugging purposes.
96
+ * Replaces special characters with their Unicode escape sequences or names.
97
+ * Useful for showing users where problematic characters are in their input.
98
+ * @param {string} text - Text to make visible
99
+ * @param {Object} options - Configuration options
100
+ * @param {number} options.maxLength - Maximum length of output (default: 200)
101
+ * @returns {string} Text with special characters made visible
102
+ */
103
+ export function makeSpecialCharsVisible(text, options = {}) {
104
+ if (!text || typeof text !== 'string') return text;
105
+
106
+ const { maxLength = 200 } = options;
107
+
108
+ // Map of special characters to their visible representations
109
+ const specialChars = {
110
+ '\u200B': '[ZWSP]', // Zero-width space
111
+ '\u200C': '[ZWNJ]', // Zero-width non-joiner
112
+ '\u200D': '[ZWJ]', // Zero-width joiner
113
+ '\uFEFF': '[BOM]', // Byte order mark / zero-width no-break space
114
+ '\u00AD': '[SHY]', // Soft hyphen
115
+ '\t': '[TAB]',
116
+ '\r': '[CR]',
117
+ '\n': '[LF]',
118
+ };
119
+
120
+ let result = '';
121
+ for (let i = 0; i < text.length && result.length < maxLength; i++) {
122
+ const char = text[i];
123
+ const code = char.charCodeAt(0);
124
+
125
+ if (specialChars[char]) {
126
+ result += specialChars[char];
127
+ } else if (code < 32 || (code >= 127 && code < 160)) {
128
+ // Control characters
129
+ result += `[U+${code.toString(16).toUpperCase().padStart(4, '0')}]`;
130
+ } else {
131
+ result += char;
132
+ }
133
+ }
134
+
135
+ if (text.length > maxLength) {
136
+ result += '... (truncated)';
137
+ }
138
+
139
+ return result;
140
+ }