@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 +33 -0
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/option-suggestions.lib.mjs +180 -0
- package/src/solve.config.lib.mjs +28 -4
- package/src/solve.mjs +10 -1
- package/src/telegram-bot.mjs +65 -64
- package/src/telegram-markdown.lib.mjs +76 -0
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
|
|
336
|
-
|
|
|
337
|
-
| `--model`
|
|
338
|
-
| `--think`
|
|
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
|
@@ -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
|
+
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
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),
|
|
349
|
-
if
|
|
350
|
-
|
|
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
|
-
|
|
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
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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 += 'š§ *
|
|
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 += '
|
|
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
|
-
//
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
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
|
|
1381
|
-
|
|
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
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
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
|
+
}
|