@link-assistant/hive-mind 2.0.13 → 2.0.14
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
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getenv, makeConfig, yargs as linoYargs } from 'lino-arguments';
|
|
2
2
|
|
|
3
|
+
import { enhanceUnknownArgumentError } from './option-suggestions.lib.mjs';
|
|
4
|
+
|
|
3
5
|
export { getenv };
|
|
4
6
|
|
|
5
7
|
export const hideBin = argv => argv.slice(2);
|
|
@@ -52,17 +54,24 @@ export function addCliCompatibilityAliases(parsed, { positionalAliases = [] } =
|
|
|
52
54
|
|
|
53
55
|
export function parseCliArgumentsWithLino({ argv = process.argv, commandName = 'cli', createYargsConfig, positionalAliases = [], lenv = { enabled: true }, env = { enabled: false }, getenv: getenvOptions = { enabled: true } } = {}) {
|
|
54
56
|
const fullArgv = ensureFullArgv(argv, commandName);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
let configuredParser = null;
|
|
58
|
+
let parsed;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
parsed = makeConfig({
|
|
62
|
+
argv: fullArgv,
|
|
63
|
+
lenv,
|
|
64
|
+
env,
|
|
65
|
+
getenv: getenvOptions,
|
|
66
|
+
yargs: ({ yargs, getenv: getenvHelper }) => {
|
|
67
|
+
const parser = yargs.exitProcess(false);
|
|
68
|
+
configuredParser = createYargsConfig ? createYargsConfig(parser, getenvHelper) : parser;
|
|
69
|
+
return configuredParser.exitProcess(false);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw enhanceUnknownArgumentError(error, configuredParser);
|
|
74
|
+
}
|
|
66
75
|
|
|
67
76
|
return addCliCompatibilityAliases(parsed, { positionalAliases });
|
|
68
77
|
}
|
|
@@ -44,79 +44,97 @@ export function calculateLevenshteinDistance(a, b) {
|
|
|
44
44
|
return matrix[b.length][a.length];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(/^-+/, '');
|
|
47
|
+
const normalizeOptionName = option =>
|
|
48
|
+
String(option || '')
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/^-+/, '')
|
|
51
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
52
|
+
.replace(/[_\s]+/g, '-')
|
|
53
|
+
.toLowerCase();
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
const compactOptionName = option => normalizeOptionName(option).replace(/-/g, '');
|
|
56
|
+
|
|
57
|
+
function getAvailableOptionNames(yargsInstance, includeShortAliases) {
|
|
61
58
|
const availableOptions = yargsInstance.getOptions();
|
|
62
59
|
const allOptions = new Set();
|
|
60
|
+
const rawHiddenOptions = availableOptions.hiddenOptions || [];
|
|
61
|
+
const hiddenOptionNames = Array.isArray(rawHiddenOptions) ? rawHiddenOptions : Object.keys(rawHiddenOptions);
|
|
62
|
+
const hiddenOptions = new Set(hiddenOptionNames.map(normalizeOptionName));
|
|
63
|
+
|
|
64
|
+
const addOption = option => {
|
|
65
|
+
const normalized = normalizeOptionName(option);
|
|
66
|
+
if (!normalized || hiddenOptions.has(normalized)) return;
|
|
67
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) return;
|
|
68
|
+
if (!includeShortAliases && normalized.length === 1) return;
|
|
69
|
+
allOptions.add(normalized);
|
|
70
|
+
};
|
|
63
71
|
|
|
64
|
-
// Collect all option names (both long form and aliases)
|
|
65
72
|
if (availableOptions.key) {
|
|
66
|
-
// Ensure it's an array before iterating
|
|
67
73
|
const keys = Array.isArray(availableOptions.key) ? availableOptions.key : Object.keys(availableOptions.key || {});
|
|
68
|
-
keys.forEach(
|
|
69
|
-
allOptions.add(opt);
|
|
70
|
-
});
|
|
74
|
+
keys.forEach(addOption);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
// Collect aliases
|
|
74
77
|
if (availableOptions.alias) {
|
|
75
78
|
Object.entries(availableOptions.alias).forEach(([key, aliases]) => {
|
|
76
|
-
|
|
79
|
+
addOption(key);
|
|
77
80
|
if (Array.isArray(aliases)) {
|
|
78
|
-
aliases.forEach(
|
|
81
|
+
aliases.forEach(addOption);
|
|
79
82
|
} else if (aliases) {
|
|
80
|
-
|
|
81
|
-
allOptions.add(String(aliases));
|
|
83
|
+
addOption(aliases);
|
|
82
84
|
}
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
return allOptions;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Find similar option names based on Levenshtein distance
|
|
93
|
+
*
|
|
94
|
+
* @param {string} unknownOption - The option name that was not recognized (e.g., "branch")
|
|
95
|
+
* @param {Object} yargsInstance - The yargs instance with defined options
|
|
96
|
+
* @param {number} maxSuggestions - Maximum number of suggestions to return (default: 5)
|
|
97
|
+
* @param {number} distanceThreshold - Maximum distance to consider for suggestions (default: 5)
|
|
98
|
+
* @returns {string[]} - Array of suggested option names, sorted by similarity
|
|
99
|
+
*/
|
|
100
|
+
export function findSimilarOptions(unknownOption, yargsInstance, maxSuggestions = 5, distanceThreshold = 5) {
|
|
101
|
+
const cleanUnknown = normalizeOptionName(unknownOption);
|
|
102
|
+
const compactUnknown = compactOptionName(unknownOption);
|
|
103
|
+
const includeShortAliases = cleanUnknown.length === 1;
|
|
104
|
+
const allOptions = getAvailableOptionNames(yargsInstance, includeShortAliases);
|
|
105
|
+
|
|
87
106
|
const distances = [];
|
|
88
107
|
allOptions.forEach(option => {
|
|
89
|
-
const distance = calculateLevenshteinDistance(cleanUnknown, option);
|
|
90
|
-
|
|
91
|
-
|
|
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;
|
|
108
|
+
const distance = Math.min(calculateLevenshteinDistance(cleanUnknown, option), calculateLevenshteinDistance(compactUnknown, compactOptionName(option)));
|
|
109
|
+
const unknownInOption = option.includes(cleanUnknown) || compactOptionName(option).includes(compactUnknown);
|
|
110
|
+
const optionInUnknown = cleanUnknown.includes(option) || compactUnknown.includes(compactOptionName(option));
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
if (distance <= distanceThreshold || unknownInOption || optionInUnknown) {
|
|
113
|
+
const substringBonus = unknownInOption ? -10 : optionInUnknown ? -5 : 0;
|
|
102
114
|
const lengthDiff = Math.abs(option.length - cleanUnknown.length);
|
|
103
115
|
const lengthBonus = lengthDiff < 3 ? -1 : 0;
|
|
104
116
|
|
|
105
117
|
distances.push({
|
|
106
118
|
option,
|
|
107
119
|
distance,
|
|
120
|
+
lengthDiff,
|
|
108
121
|
effectiveDistance: distance + substringBonus + lengthBonus,
|
|
109
122
|
});
|
|
110
123
|
}
|
|
111
124
|
});
|
|
112
125
|
|
|
113
|
-
// Sort by effective distance (closest first), then by actual distance
|
|
114
126
|
return distances
|
|
115
127
|
.sort((a, b) => {
|
|
116
128
|
if (a.effectiveDistance !== b.effectiveDistance) {
|
|
117
129
|
return a.effectiveDistance - b.effectiveDistance;
|
|
118
130
|
}
|
|
119
|
-
|
|
131
|
+
if (a.distance !== b.distance) {
|
|
132
|
+
return a.distance - b.distance;
|
|
133
|
+
}
|
|
134
|
+
if (a.lengthDiff !== b.lengthDiff) {
|
|
135
|
+
return a.lengthDiff - b.lengthDiff;
|
|
136
|
+
}
|
|
137
|
+
return a.option.localeCompare(b.option);
|
|
120
138
|
})
|
|
121
139
|
.slice(0, maxSuggestions)
|
|
122
140
|
.map(item => item.option);
|
|
@@ -133,17 +151,17 @@ export function formatSuggestions(suggestions) {
|
|
|
133
151
|
return '';
|
|
134
152
|
}
|
|
135
153
|
|
|
136
|
-
if (suggestions.length === 1) {
|
|
137
|
-
return `\n\nDid you mean --${suggestions[0]}?`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// For multiple suggestions, format them nicely
|
|
141
154
|
const formattedOptions = suggestions.map(opt => {
|
|
142
155
|
// If it's a single character, show as -x, otherwise --option-name
|
|
143
156
|
return opt.length === 1 ? `-${opt}` : `--${opt}`;
|
|
144
157
|
});
|
|
158
|
+
const [primarySuggestion, ...alternatives] = formattedOptions;
|
|
145
159
|
|
|
146
|
-
|
|
160
|
+
if (alternatives.length === 0) {
|
|
161
|
+
return `\n\nDid you mean \`${primarySuggestion}\` option?`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return `\n\nDid you mean \`${primarySuggestion}\` option?\n\nOther close matches:\n${alternatives.map(opt => ` • \`${opt}\``).join('\n')}`;
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
/**
|
|
@@ -300,9 +318,13 @@ export function detectMalformedFlags(args) {
|
|
|
300
318
|
* @returns {string} - Enhanced error message with suggestions
|
|
301
319
|
*/
|
|
302
320
|
export function enhanceErrorMessage(originalError, yargsInstance) {
|
|
321
|
+
if (/Did you mean/i.test(originalError)) {
|
|
322
|
+
return originalError;
|
|
323
|
+
}
|
|
324
|
+
|
|
303
325
|
// Extract the unknown option name from the error message
|
|
304
326
|
// Typical format: "Unknown argument: branch" or "Unknown arguments: branch, test"
|
|
305
|
-
const unknownMatch = originalError.match(/Unknown arguments?:\s*(
|
|
327
|
+
const unknownMatch = originalError.match(/Unknown arguments?:\s*([^\n]+)/i);
|
|
306
328
|
|
|
307
329
|
if (!unknownMatch) {
|
|
308
330
|
return originalError;
|
|
@@ -324,3 +346,23 @@ export function enhanceErrorMessage(originalError, yargsInstance) {
|
|
|
324
346
|
|
|
325
347
|
return enhancedMessage;
|
|
326
348
|
}
|
|
349
|
+
|
|
350
|
+
export function enhanceUnknownArgumentError(error, yargsInstance) {
|
|
351
|
+
if (!error || error._enhanced || !yargsInstance || !/Unknown arguments?/i.test(error.message || '')) {
|
|
352
|
+
return error;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const enhancedMessage = enhanceErrorMessage(error.message, yargsInstance);
|
|
356
|
+
if (enhancedMessage === error.message) {
|
|
357
|
+
return error;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const enhancedError = new Error(enhancedMessage);
|
|
361
|
+
enhancedError.name = error.name;
|
|
362
|
+
enhancedError.cause = error;
|
|
363
|
+
for (const key of Object.keys(error)) {
|
|
364
|
+
enhancedError[key] = error[key];
|
|
365
|
+
}
|
|
366
|
+
enhancedError._enhanced = true;
|
|
367
|
+
return enhancedError;
|
|
368
|
+
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -25,6 +25,7 @@ await loadLenvConfig({ override: true, quiet: true });
|
|
|
25
25
|
const yargs = getLinoYargsFactory();
|
|
26
26
|
const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
|
|
27
27
|
const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
|
|
28
|
+
const { enhanceUnknownArgumentError } = await import('./option-suggestions.lib.mjs');
|
|
28
29
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
29
30
|
const { extractIsolationFromArgs, isValidPerCommandIsolation } = await import('./telegram-isolation.lib.mjs');
|
|
30
31
|
|
|
@@ -229,7 +230,8 @@ if (solveEnabled && solveOverrides.length > 0) {
|
|
|
229
230
|
process.stderr.write = originalStderrWrite;
|
|
230
231
|
}
|
|
231
232
|
} catch (error) {
|
|
232
|
-
|
|
233
|
+
const enhancedError = enhanceUnknownArgumentError(error, createSolveYargsConfig(yargs()));
|
|
234
|
+
console.error(`❌ Invalid solve-overrides: ${enhancedError.message || String(enhancedError)}`);
|
|
233
235
|
console.error(` Overrides: ${solveOverrides.join(' ')}`);
|
|
234
236
|
process.exit(1);
|
|
235
237
|
}
|
|
@@ -275,7 +277,8 @@ if (hiveEnabled && hiveOverrides.length > 0) {
|
|
|
275
277
|
process.stderr.write = originalStderrWrite;
|
|
276
278
|
}
|
|
277
279
|
} catch (error) {
|
|
278
|
-
|
|
280
|
+
const enhancedError = enhanceUnknownArgumentError(error, createHiveYargsConfig(yargs()));
|
|
281
|
+
console.error(`❌ Invalid hive-overrides: ${enhancedError.message || String(enhancedError)}`);
|
|
279
282
|
console.error(` Overrides: ${hiveOverrides.join(' ')}`);
|
|
280
283
|
process.exit(1);
|
|
281
284
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @see https://github.com/link-assistant/hive-mind/issues/1618
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { enhanceUnknownArgumentError } from './option-suggestions.lib.mjs';
|
|
12
|
+
|
|
11
13
|
export const TOOL_SOLVE_COMMAND_ALIASES = Object.freeze({
|
|
12
14
|
claude: 'claude',
|
|
13
15
|
codex: 'codex',
|
|
@@ -89,8 +91,9 @@ export async function parseArgsWithYargs(args, yargsFactory, createYargsConfig)
|
|
|
89
91
|
else if (typeof callback === 'function') callback();
|
|
90
92
|
return true;
|
|
91
93
|
};
|
|
94
|
+
let parser = null;
|
|
92
95
|
try {
|
|
93
|
-
|
|
96
|
+
parser = createYargsConfig(yargsFactory());
|
|
94
97
|
parser
|
|
95
98
|
.exitProcess(false)
|
|
96
99
|
.showHelpOnFail(false)
|
|
@@ -98,6 +101,8 @@ export async function parseArgsWithYargs(args, yargsFactory, createYargsConfig)
|
|
|
98
101
|
throw err || new Error(msg || 'Invalid arguments');
|
|
99
102
|
});
|
|
100
103
|
return await parser.parse(args);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw enhanceUnknownArgumentError(error, parser);
|
|
101
106
|
} finally {
|
|
102
107
|
process.stderr.write = originalStderrWrite;
|
|
103
108
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { buildUserMention } from './buildUserMention.lib.mjs';
|
|
2
2
|
import { validateModelName } from './models/index.mjs';
|
|
3
|
+
import { getLinoYargsFactory } from './cli-arguments.lib.mjs';
|
|
4
|
+
import { createYargsConfig as createTaskYargsConfig } from './task.config.lib.mjs';
|
|
3
5
|
import { createTaskIssue, parseTaskIssueCreationInput, resolveTaskIssueCreationInput } from './task.issue-creation.lib.mjs';
|
|
4
6
|
import { parseTaskIssueUrl } from './task.split.lib.mjs';
|
|
5
7
|
import { escapeMarkdown } from './telegram-markdown.lib.mjs';
|
|
6
8
|
import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
|
|
7
|
-
import { moveArgumentToFront, parseCommandArgs } from './telegram-solve-command.lib.mjs';
|
|
9
|
+
import { moveArgumentToFront, parseArgsWithYargs, parseCommandArgs } from './telegram-solve-command.lib.mjs';
|
|
8
10
|
import { formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
|
|
9
11
|
|
|
10
12
|
export const TASK_COMMAND_NAMES = Object.freeze(['task', 'split']);
|
|
@@ -187,6 +189,13 @@ export function registerTaskCommands(bot, options) {
|
|
|
187
189
|
return;
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
try {
|
|
193
|
+
await parseArgsWithYargs(filteredArgs, getLinoYargsFactory(), createTaskYargsConfig);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, { reply_to_message_id: ctx.message.message_id });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
190
199
|
const modelError = validateTaskModel(filteredArgs);
|
|
191
200
|
if (modelError) {
|
|
192
201
|
await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
|