@link-assistant/hive-mind 0.54.4 → 0.54.6

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,38 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.54.6
4
+
5
+ ### Patch Changes
6
+
7
+ - f734d5d: feat: Add --base-branch to /help and implement option typo suggestions
8
+ - Added --base-branch option to Telegram bot /help command
9
+ - Implemented intelligent option name suggestions using Levenshtein distance
10
+ - Added --base-branch to README.md solve options section
11
+ - Enhanced error messages with helpful suggestions for typos (e.g., --branch → --base-branch)
12
+
13
+ ## 0.54.5
14
+
15
+ ### Patch Changes
16
+
17
+ - Fix duplicate APT sources warning in installation script
18
+ - Add `cleanup_duplicate_apt_sources()` function to detect and remove duplicate APT source files
19
+ - Clean up duplicate Microsoft Edge sources (`microsoft-edge.list` vs `microsoft-edge-stable.list`)
20
+ - Clean up duplicate Google Chrome sources (`google-chrome.list` vs `google-chrome-stable.list`)
21
+ - Run cleanup before `apt update` to prevent "Target Packages configured multiple times" warnings
22
+ - Ensures script supports clean upgrade mode when run on previously installed systems
23
+
24
+ Improve Telegram bot error messages for better user experience (issue #1070)
25
+ - Enhanced URL validation to provide specific, actionable error messages based on URL type (issues list, pulls list, repository)
26
+ - Added step-by-step fix instructions with examples when users provide wrong URL formats
27
+ - Improved global error handler to properly escape Markdown special characters, preventing "400: Bad Request: can't parse entities" errors
28
+ - Added special handling for Telegram API parsing errors with clearer messaging
29
+ - Added `cleanNonPrintableChars()` to automatically remove invisible Unicode characters from user input
30
+ - Added `makeSpecialCharsVisible()` to show users exactly where problematic special characters are in their input
31
+ - Enhanced error messages to display user input with special characters made visible for easier debugging
32
+ - Refactored telegram-bot.mjs to meet 1500 line limit requirement
33
+ - Created comprehensive test suites to verify URL validation improvements and special character handling
34
+ - Documented case study analysis in docs/case-studies/issue-1070/ANALYSIS.md
35
+
3
36
  ## 0.54.4
4
37
 
5
38
  ### Patch Changes
package/README.md CHANGED
@@ -332,10 +332,11 @@ solve <issue-url> [options]
332
332
 
333
333
  **Most frequently used options:**
334
334
 
335
- | Option | Alias | Description | Default |
336
- | --------- | ----- | --------------------------------------- | ------- |
337
- | `--model` | `-m` | AI model to use (sonnet, opus, haiku) | sonnet |
338
- | `--think` | | Thinking level (low, medium, high, max) | - |
335
+ | Option | Alias | Description | Default |
336
+ | --------------- | ----- | --------------------------------------- | --------- |
337
+ | `--model` | `-m` | AI model to use (sonnet, opus, haiku) | sonnet |
338
+ | `--think` | | Thinking level (low, medium, high, max) | - |
339
+ | `--base-branch` | `-b` | Target branch for PR | (default) |
339
340
 
340
341
  **Other useful options:**
341
342
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.54.4",
3
+ "version": "0.54.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,180 @@
1
+ // Option suggestion utility for providing helpful error messages
2
+ // when users mistype command-line option names
3
+
4
+ /**
5
+ * Calculate Levenshtein distance between two strings
6
+ * Measures the minimum number of single-character edits (insertions, deletions, or substitutions)
7
+ * required to change one string into the other.
8
+ *
9
+ * @param {string} a - First string
10
+ * @param {string} b - Second string
11
+ * @returns {number} - Levenshtein distance
12
+ */
13
+ export function calculateLevenshteinDistance(a, b) {
14
+ if (a.length === 0) return b.length;
15
+ if (b.length === 0) return a.length;
16
+
17
+ const matrix = [];
18
+
19
+ // Initialize first column of matrix
20
+ for (let i = 0; i <= b.length; i++) {
21
+ matrix[i] = [i];
22
+ }
23
+
24
+ // Initialize first row of matrix
25
+ for (let j = 0; j <= a.length; j++) {
26
+ matrix[0][j] = j;
27
+ }
28
+
29
+ // Fill in the rest of the matrix
30
+ for (let i = 1; i <= b.length; i++) {
31
+ for (let j = 1; j <= a.length; j++) {
32
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
33
+ matrix[i][j] = matrix[i - 1][j - 1];
34
+ } else {
35
+ matrix[i][j] = Math.min(
36
+ matrix[i - 1][j - 1] + 1, // substitution
37
+ matrix[i][j - 1] + 1, // insertion
38
+ matrix[i - 1][j] + 1 // deletion
39
+ );
40
+ }
41
+ }
42
+ }
43
+
44
+ return matrix[b.length][a.length];
45
+ }
46
+
47
+ /**
48
+ * Find similar option names based on Levenshtein distance
49
+ *
50
+ * @param {string} unknownOption - The option name that was not recognized (e.g., "branch")
51
+ * @param {Object} yargsInstance - The yargs instance with defined options
52
+ * @param {number} maxSuggestions - Maximum number of suggestions to return (default: 3)
53
+ * @param {number} distanceThreshold - Maximum distance to consider for suggestions (default: 5)
54
+ * @returns {string[]} - Array of suggested option names, sorted by similarity
55
+ */
56
+ export function findSimilarOptions(unknownOption, yargsInstance, maxSuggestions = 3, distanceThreshold = 5) {
57
+ // Remove leading dashes from the unknown option
58
+ const cleanUnknown = unknownOption.replace(/^-+/, '');
59
+
60
+ // Get all available options from yargs
61
+ const availableOptions = yargsInstance.getOptions();
62
+ const allOptions = new Set();
63
+
64
+ // Collect all option names (both long form and aliases)
65
+ if (availableOptions.key) {
66
+ // Ensure it's an array before iterating
67
+ const keys = Array.isArray(availableOptions.key) ? availableOptions.key : Object.keys(availableOptions.key || {});
68
+ keys.forEach(opt => {
69
+ allOptions.add(opt);
70
+ });
71
+ }
72
+
73
+ // Collect aliases
74
+ if (availableOptions.alias) {
75
+ Object.entries(availableOptions.alias).forEach(([key, aliases]) => {
76
+ allOptions.add(key);
77
+ if (Array.isArray(aliases)) {
78
+ aliases.forEach(alias => allOptions.add(alias));
79
+ } else if (aliases) {
80
+ // If it's not an array but exists, add it as a single alias
81
+ allOptions.add(String(aliases));
82
+ }
83
+ });
84
+ }
85
+
86
+ // Calculate distance for each option
87
+ const distances = [];
88
+ allOptions.forEach(option => {
89
+ const distance = calculateLevenshteinDistance(cleanUnknown, option);
90
+ if (distance <= distanceThreshold) {
91
+ // Calculate bonus score for substring matches
92
+ // If the unknown option is a substring of the valid option, it's likely what the user meant
93
+ // Check both directions: is unknown a substring of option, or is option a substring of unknown
94
+ const unknownInOption = option.includes(cleanUnknown);
95
+ const optionInUnknown = cleanUnknown.includes(option);
96
+
97
+ // Strong bonus for when user typed a word that appears in the option name
98
+ // e.g., "branch" appears in "base-branch"
99
+ const substringBonus = unknownInOption ? -10 : optionInUnknown ? -5 : 0;
100
+
101
+ // Also prioritize options with similar length (user likely tried to type the full name)
102
+ const lengthDiff = Math.abs(option.length - cleanUnknown.length);
103
+ const lengthBonus = lengthDiff < 3 ? -1 : 0;
104
+
105
+ distances.push({
106
+ option,
107
+ distance,
108
+ effectiveDistance: distance + substringBonus + lengthBonus,
109
+ });
110
+ }
111
+ });
112
+
113
+ // Sort by effective distance (closest first), then by actual distance
114
+ return distances
115
+ .sort((a, b) => {
116
+ if (a.effectiveDistance !== b.effectiveDistance) {
117
+ return a.effectiveDistance - b.effectiveDistance;
118
+ }
119
+ return a.distance - b.distance;
120
+ })
121
+ .slice(0, maxSuggestions)
122
+ .map(item => item.option);
123
+ }
124
+
125
+ /**
126
+ * Format suggestions into a user-friendly error message
127
+ *
128
+ * @param {string[]} suggestions - Array of suggested option names
129
+ * @returns {string} - Formatted suggestion message
130
+ */
131
+ export function formatSuggestions(suggestions) {
132
+ if (suggestions.length === 0) {
133
+ return '';
134
+ }
135
+
136
+ if (suggestions.length === 1) {
137
+ return `\n\nDid you mean --${suggestions[0]}?`;
138
+ }
139
+
140
+ // For multiple suggestions, format them nicely
141
+ const formattedOptions = suggestions.map(opt => {
142
+ // If it's a single character, show as -x, otherwise --option-name
143
+ return opt.length === 1 ? `-${opt}` : `--${opt}`;
144
+ });
145
+
146
+ return `\n\nDid you mean one of these?\n${formattedOptions.map(opt => ` • ${opt}`).join('\n')}`;
147
+ }
148
+
149
+ /**
150
+ * Create an enhanced error message with suggestions for unknown arguments
151
+ *
152
+ * @param {string} originalError - The original error message from yargs
153
+ * @param {Object} yargsInstance - The yargs instance with defined options
154
+ * @returns {string} - Enhanced error message with suggestions
155
+ */
156
+ export function enhanceErrorMessage(originalError, yargsInstance) {
157
+ // Extract the unknown option name from the error message
158
+ // Typical format: "Unknown argument: branch" or "Unknown arguments: branch, test"
159
+ const unknownMatch = originalError.match(/Unknown arguments?:\s*(.+?)(?:\s|$)/i);
160
+
161
+ if (!unknownMatch) {
162
+ return originalError;
163
+ }
164
+
165
+ // Get the first unknown argument (if multiple, focus on the first)
166
+ const unknownArgs = unknownMatch[1].split(',').map(arg => arg.trim());
167
+ const firstUnknown = unknownArgs[0];
168
+
169
+ // Find similar options
170
+ const suggestions = findSimilarOptions(firstUnknown, yargsInstance);
171
+
172
+ // Format the enhanced message
173
+ let enhancedMessage = originalError;
174
+
175
+ if (suggestions.length > 0) {
176
+ enhancedMessage += formatSuggestions(suggestions);
177
+ }
178
+
179
+ return enhancedMessage;
180
+ }
@@ -7,6 +7,8 @@
7
7
  // Note: Strict options validation is now handled by yargs built-in .strict() mode (see below)
8
8
  // This approach was adopted per issue #482 feedback to minimize custom code maintenance
9
9
 
10
+ import { enhanceErrorMessage } from './option-suggestions.lib.mjs';
11
+
10
12
  // Export an initialization function that accepts 'use'
11
13
  export const initializeConfig = async use => {
12
14
  // Import yargs with specific version for hideBin support
@@ -310,6 +312,7 @@ export const parseArguments = async (yargs, hideBin) => {
310
312
  // See: https://github.com/yargs/yargs/issues - .strict() only works with .parse()
311
313
 
312
314
  let argv;
315
+ let yargsInstance;
313
316
  try {
314
317
  // Suppress stderr output from yargs during parsing to prevent validation errors from appearing
315
318
  // This prevents "YError: Not enough arguments" from polluting stderr (issue #583)
@@ -330,7 +333,8 @@ export const parseArguments = async (yargs, hideBin) => {
330
333
  };
331
334
 
332
335
  try {
333
- argv = await createYargsConfig(yargs()).parse(rawArgs);
336
+ yargsInstance = createYargsConfig(yargs());
337
+ argv = await yargsInstance.parse(rawArgs);
334
338
  } finally {
335
339
  // Always restore stderr.write
336
340
  process.stderr.write = originalStderrWrite;
@@ -345,9 +349,29 @@ export const parseArguments = async (yargs, hideBin) => {
345
349
  }
346
350
  } catch (error) {
347
351
  // Yargs throws errors for validation issues
348
- // If the error is about unknown arguments (strict mode), re-throw it
349
- if (error.message && error.message.includes('Unknown arguments')) {
350
- throw error;
352
+ // If the error is about unknown arguments (strict mode), enhance it with suggestions
353
+ // Check if this error has already been enhanced to avoid re-processing
354
+ if (error.message && /Unknown argument/.test(error.message) && !error._enhanced) {
355
+ try {
356
+ // Enhance the error message with helpful suggestions
357
+ // Use the yargsInstance we already created, or create a new one if needed
358
+ const yargsWithConfig = yargsInstance || createYargsConfig(yargs());
359
+ const enhancedMessage = enhanceErrorMessage(error.message, yargsWithConfig);
360
+ const enhancedError = new Error(enhancedMessage);
361
+ enhancedError.name = error.name;
362
+ enhancedError._enhanced = true; // Mark as enhanced to prevent re-processing
363
+ throw enhancedError;
364
+ } catch (enhanceErr) {
365
+ // If enhancing fails, just throw the original error
366
+ if (global.verboseMode) {
367
+ console.error('[VERBOSE] Failed to enhance error message:', enhanceErr.message);
368
+ }
369
+ // If the enhance error itself is already enhanced, throw it
370
+ if (enhanceErr._enhanced) {
371
+ throw enhanceErr;
372
+ }
373
+ throw error;
374
+ }
351
375
  }
352
376
  // For other validation errors, show a warning in verbose mode
353
377
  if (error.message && global.verboseMode) {
package/src/solve.mjs CHANGED
@@ -104,7 +104,16 @@ await log('šŸ”§ Raw command executed:');
104
104
  await log(` ${rawCommand}`);
105
105
  await log('');
106
106
 
107
- const argv = await parseArguments(yargs, hideBin);
107
+ let argv;
108
+ try {
109
+ argv = await parseArguments(yargs, hideBin);
110
+ } catch (error) {
111
+ // Handle argument parsing errors with helpful messages
112
+ await log(`āŒ ${error.message}`, { level: 'error' });
113
+ await log('', { level: 'error' });
114
+ await log('Use /help to see available options', { level: 'error' });
115
+ await safeExit(1, 'Invalid command-line arguments');
116
+ }
108
117
  global.verboseMode = argv.verbose;
109
118
 
110
119
  // If user specified a custom log directory, we would need to move the log file
@@ -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))
@@ -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
 
@@ -796,11 +807,12 @@ bot.command('help', async ctx => {
796
807
  message += '*/version* - Show bot and runtime versions\n';
797
808
  message += '*/help* - Show this help message\n\n';
798
809
  message += 'āš ļø *Note:* /solve, /hive, /limits and /version commands only work in group chats.\n\n';
799
- message += 'šŸ”§ *Available Options:*\n';
800
- message += '• `--model <model>` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
810
+ message += 'šŸ”§ *Common Options:*\n';
811
+ message += '• `--model <model>` or `-m` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
812
+ message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
801
813
  message += '• `--think <level>` - Thinking level (low/medium/high/max)\n';
802
- message += '• `--verbose` - Verbose output\n';
803
- message += '• `--attach-logs` - Attach logs to PR\n';
814
+ message += '• `--verbose` or `-v` - Verbose output | `--attach-logs` - Attach logs to PR\n';
815
+ message += '\nšŸ’” *Tip:* Many more options available. See full documentation for complete list.\n';
804
816
 
805
817
  if (allowedChats) {
806
818
  message += '\nšŸ”’ *Restricted Mode:* This bot only accepts commands from authorized chats.\n';
@@ -1313,38 +1325,46 @@ bot.catch((error, ctx) => {
1313
1325
 
1314
1326
  // Try to notify the user about the error with more details
1315
1327
  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}`;
1328
+ // Detect if this is a Telegram API parsing error
1329
+ 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')));
1330
+
1331
+ let errorMessage;
1332
+
1333
+ if (isTelegramParsingError) {
1334
+ // Special handling for Telegram API parsing errors caused by unescaped special characters
1335
+ 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.`;
1336
+ // Show the user's input with special characters visible (if available)
1337
+ if (ctx.message?.text) {
1338
+ const cleanedInput = cleanNonPrintableChars(ctx.message.text);
1339
+ const visibleInput = makeSpecialCharsVisible(cleanedInput, { maxLength: 150 });
1340
+ if (visibleInput !== cleanedInput) errorMessage += `\n\nšŸ“ Your input (with special chars visible):\n\`${escapeMarkdown(visibleInput)}\``;
1341
+ }
1342
+ if (VERBOSE) {
1343
+ const escapedError = escapeMarkdown(error.message || 'Unknown error');
1344
+ errorMessage += `\n\nšŸ” Debug info: ${escapedError}\nUpdate ID: ${ctx.update.update_id}`;
1345
+ }
1346
+ } else {
1347
+ // Build informative error message for other errors
1348
+ errorMessage = 'āŒ An error occurred while processing your request.\n\n';
1349
+ if (error.message) {
1350
+ // Filter out sensitive info and escape markdown
1351
+ const sanitizedMessage = escapeMarkdown(
1352
+ error.message
1353
+ .replace(/token[s]?\s*[:=]\s*[\w-]+/gi, 'token: [REDACTED]')
1354
+ .replace(/password[s]?\s*[:=]\s*[\w-]+/gi, 'password: [REDACTED]')
1355
+ .replace(/api[_-]?key[s]?\s*[:=]\s*[\w-]+/gi, 'api_key: [REDACTED]')
1356
+ );
1357
+ errorMessage += `Details: ${sanitizedMessage}\n`;
1358
+ }
1359
+ 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';
1360
+ if (VERBOSE) errorMessage += `\n\nšŸ” Debug info: Update ID: ${ctx.update.update_id}`;
1342
1361
  }
1343
1362
 
1344
1363
  ctx.reply(errorMessage, { parse_mode: 'Markdown' }).catch(replyError => {
1345
1364
  console.error('Failed to send error message to user:', replyError);
1346
1365
  // 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 => {
1366
+ const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
1367
+ ctx.reply(plainMessage).catch(fallbackError => {
1348
1368
  console.error('Failed to send fallback error message:', fallbackError);
1349
1369
  });
1350
1370
  });
@@ -1361,29 +1381,17 @@ if (allowedChats && allowedChats.length > 0) {
1361
1381
  } else {
1362
1382
  console.log('Allowed chats: All (no restrictions)');
1363
1383
  }
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
- }
1384
+ console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled });
1385
+ if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
1386
+ if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1374
1387
  if (VERBOSE) {
1375
1388
  console.log('[VERBOSE] Verbose logging enabled');
1376
1389
  console.log('[VERBOSE] Bot start time (Unix):', BOT_START_TIME);
1377
1390
  console.log('[VERBOSE] Bot start time (ISO):', new Date(BOT_START_TIME * 1000).toISOString());
1378
1391
  }
1379
1392
 
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
- }
1393
+ // Delete existing webhook (critical: webhooks prevent polling from working)
1394
+ if (VERBOSE) console.log('[VERBOSE] Deleting webhook...');
1387
1395
  bot.telegram
1388
1396
  .deleteWebhook({ drop_pending_updates: true })
1389
1397
  .then(result => {
@@ -1398,19 +1406,12 @@ bot.telegram
1398
1406
  });
1399
1407
  }
1400
1408
  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,
1409
+ allowedUpdates: ['message', 'callback_query'], // Receive messages and callback queries
1410
+ dropPendingUpdates: true, // Drop pending updates sent before bot started
1407
1411
  });
1408
1412
  })
1409
1413
  .then(async () => {
1410
- // Check if shutdown was initiated before printing success messages
1411
- if (isShuttingDown) {
1412
- return; // Skip success messages if shutting down
1413
- }
1414
+ if (isShuttingDown) return; // Skip success messages if shutting down
1414
1415
 
1415
1416
  console.log('āœ… SwarmMindBot is now running!');
1416
1417
  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
+ }