mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 16:46:58 +10:00
147 lines
21 KiB
TypeScript
147 lines
21 KiB
TypeScript
|
|
import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js';
|
||
|
|
import { extractOutputRedirections } from '../../../utils/bash/commands.js';
|
||
|
|
import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js';
|
||
|
|
import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js';
|
||
|
|
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
|
||
|
|
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
|
||
|
|
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||
|
|
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js';
|
||
|
|
export type BashToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'yes-classifier-reviewed' | 'no';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a description already exists in the allow list.
|
||
|
|
* Compares lowercase and trailing-whitespace-trimmed versions.
|
||
|
|
*/
|
||
|
|
function descriptionAlreadyExists(description: string, existingDescriptions: string[]): boolean {
|
||
|
|
const normalized = description.toLowerCase().trimEnd();
|
||
|
|
return existingDescriptions.some(existing => existing.toLowerCase().trimEnd() === normalized);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Strip output redirections so filenames don't show as commands in the label.
|
||
|
|
*/
|
||
|
|
function stripBashRedirections(command: string): string {
|
||
|
|
const {
|
||
|
|
commandWithoutRedirections,
|
||
|
|
redirections
|
||
|
|
} = extractOutputRedirections(command);
|
||
|
|
// Only use stripped version if there were actual redirections
|
||
|
|
return redirections.length > 0 ? commandWithoutRedirections : command;
|
||
|
|
}
|
||
|
|
export function bashToolUseOptions({
|
||
|
|
suggestions = [],
|
||
|
|
decisionReason,
|
||
|
|
onRejectFeedbackChange,
|
||
|
|
onAcceptFeedbackChange,
|
||
|
|
onClassifierDescriptionChange,
|
||
|
|
classifierDescription,
|
||
|
|
initialClassifierDescriptionEmpty = false,
|
||
|
|
existingAllowDescriptions = [],
|
||
|
|
yesInputMode = false,
|
||
|
|
noInputMode = false,
|
||
|
|
editablePrefix,
|
||
|
|
onEditablePrefixChange
|
||
|
|
}: {
|
||
|
|
suggestions?: PermissionUpdate[];
|
||
|
|
decisionReason?: PermissionDecisionReason;
|
||
|
|
onRejectFeedbackChange: (value: string) => void;
|
||
|
|
onAcceptFeedbackChange: (value: string) => void;
|
||
|
|
onClassifierDescriptionChange?: (value: string) => void;
|
||
|
|
classifierDescription?: string;
|
||
|
|
/** Whether the initial classifier description was empty. When true, hides the option. */
|
||
|
|
initialClassifierDescriptionEmpty?: boolean;
|
||
|
|
existingAllowDescriptions?: string[];
|
||
|
|
yesInputMode?: boolean;
|
||
|
|
noInputMode?: boolean;
|
||
|
|
/** Editable prefix rule content (e.g., "npm run:*"). When set, replaces Haiku-based suggestions. */
|
||
|
|
editablePrefix?: string;
|
||
|
|
/** Callback when the user edits the prefix value. */
|
||
|
|
onEditablePrefixChange?: (value: string) => void;
|
||
|
|
}): OptionWithDescription<BashToolUseOption>[] {
|
||
|
|
const options: OptionWithDescription<BashToolUseOption>[] = [];
|
||
|
|
if (yesInputMode) {
|
||
|
|
options.push({
|
||
|
|
type: 'input',
|
||
|
|
label: 'Yes',
|
||
|
|
value: 'yes',
|
||
|
|
placeholder: 'and tell Claude what to do next',
|
||
|
|
onChange: onAcceptFeedbackChange,
|
||
|
|
allowEmptySubmitToCancel: true
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
options.push({
|
||
|
|
label: 'Yes',
|
||
|
|
value: 'yes'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only show "always allow" options when not restricted by allowManagedPermissionRulesOnly
|
||
|
|
if (shouldShowAlwaysAllowOptions()) {
|
||
|
|
// Show an editable input for the prefix rule instead of the
|
||
|
|
// Haiku-generated suggestion label — but only when the suggestions
|
||
|
|
// don't contain non-Bash items (addDirectories, Read rules) that
|
||
|
|
// the editable prefix can't represent.
|
||
|
|
const hasNonBashSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== BASH_TOOL_NAME));
|
||
|
|
if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonBashSuggestions && suggestions.length > 0) {
|
||
|
|
options.push({
|
||
|
|
type: 'input',
|
||
|
|
label: 'Yes, and don\u2019t ask again for',
|
||
|
|
value: 'yes-prefix-edited',
|
||
|
|
placeholder: 'command prefix (e.g., npm run:*)',
|
||
|
|
initialValue: editablePrefix,
|
||
|
|
onChange: onEditablePrefixChange,
|
||
|
|
allowEmptySubmitToCancel: true,
|
||
|
|
showLabelWithValue: true,
|
||
|
|
labelValueSeparator: ': ',
|
||
|
|
resetCursorOnUpdate: true
|
||
|
|
});
|
||
|
|
} else if (suggestions.length > 0) {
|
||
|
|
const label = generateShellSuggestionsLabel(suggestions, BASH_TOOL_NAME, stripBashRedirections);
|
||
|
|
if (label) {
|
||
|
|
options.push({
|
||
|
|
label,
|
||
|
|
value: 'yes-apply-suggestions'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add classifier-reviewed option if enabled, the initial description was
|
||
|
|
// non-empty, the description doesn't already exist in the allow list,
|
||
|
|
// and the decision reason is NOT a server-side classifier block
|
||
|
|
// (prompt-based rules don't help when the server-side classifier triggers first).
|
||
|
|
// Skip when the editable prefix option is already shown — they serve the
|
||
|
|
// same role and having two identical-looking "don't ask again" inputs is confusing.
|
||
|
|
const editablePrefixShown = options.some(o => o.value === 'yes-prefix-edited');
|
||
|
|
if ("external" === 'ant' && !editablePrefixShown && isClassifierPermissionsEnabled() && onClassifierDescriptionChange && !initialClassifierDescriptionEmpty && !descriptionAlreadyExists(classifierDescription ?? '', existingAllowDescriptions) && decisionReason?.type !== 'classifier') {
|
||
|
|
options.push({
|
||
|
|
type: 'input',
|
||
|
|
label: 'Yes, and don\u2019t ask again for',
|
||
|
|
value: 'yes-classifier-reviewed',
|
||
|
|
placeholder: 'describe what to allow...',
|
||
|
|
initialValue: classifierDescription ?? '',
|
||
|
|
onChange: onClassifierDescriptionChange,
|
||
|
|
allowEmptySubmitToCancel: true,
|
||
|
|
showLabelWithValue: true,
|
||
|
|
labelValueSeparator: ': ',
|
||
|
|
resetCursorOnUpdate: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (noInputMode) {
|
||
|
|
options.push({
|
||
|
|
type: 'input',
|
||
|
|
label: 'No',
|
||
|
|
value: 'no',
|
||
|
|
placeholder: 'and tell Claude what to do differently',
|
||
|
|
onChange: onRejectFeedbackChange,
|
||
|
|
allowEmptySubmitToCancel: true
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
options.push({
|
||
|
|
label: 'No',
|
||
|
|
value: 'no'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return options;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJCQVNIX1RPT0xfTkFNRSIsImV4dHJhY3RPdXRwdXRSZWRpcmVjdGlvbnMiLCJpc0NsYXNzaWZpZXJQZXJtaXNzaW9uc0VuYWJsZWQiLCJQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24iLCJQZXJtaXNzaW9uVXBkYXRlIiwic2hvdWxkU2hvd0Fsd2F5c0FsbG93T3B0aW9ucyIsIk9wdGlvbldpdGhEZXNjcmlwdGlvbiIsImdlbmVyYXRlU2hlbGxTdWdnZXN0aW9uc0xhYmVsIiwiQmFzaFRvb2xVc2VPcHRpb24iLCJkZXNjcmlwdGlvbkFscmVhZHlFeGlzdHMiLCJkZXNjcmlwdGlvbiIsImV4aXN0aW5nRGVzY3JpcHRpb25zIiwibm9ybWFsaXplZCIsInRvTG93ZXJDYXNlIiwidHJpbUVuZCIsInNvbWUiLCJleGlzdGluZyIsInN0cmlwQmFzaFJlZGlyZWN0aW9ucyIsImNvbW1hbmQiLCJjb21tYW5kV2l0aG91dFJlZGlyZWN0aW9ucyIsInJlZGlyZWN0aW9ucyIsImxlbmd0aCIsImJhc2hUb29sVXNlT3B0aW9ucyIsInN1Z2dlc3Rpb25zIiwiZGVjaXNpb25SZWFzb24iLCJvblJlamVjdEZlZWRiYWNrQ2hhbmdlIiwib25BY2NlcHRGZWVkYmFja0NoYW5nZSIsIm9uQ2xhc3NpZmllckRlc2NyaXB0aW9uQ2hhbmdlIiwiY2xhc3NpZmllckRlc2NyaXB0aW9uIiwiaW5pdGlhbENsYXNzaWZpZXJEZXNjcmlwdGlvbkVtcHR5IiwiZXhpc3RpbmdBbGxvd0Rlc2NyaXB0aW9ucyIsInllc0lucHV0TW9kZSIsIm5vSW5wdXRNb2RlIiwiZWRpdGFibGVQcmVmaXgiLCJvbkVkaXRhYmxlUHJlZml4Q2hhbmdlIiwidmFsdWUiLCJvcHRpb25zIiwicHVzaCIsInR5cGUiLCJsYWJlbCIsInBsYWNlaG9sZGVyIiwib25DaGFuZ2UiLCJhbGxvd0VtcHR5U3VibWl0VG9DYW5jZWwiLCJoYXNOb25CYXNoU3VnZ2VzdGlvbnMiLCJzIiwicnVsZXMiLCJyIiwidG9vbE5hbWUiLCJ1bmRlZmluZWQiLCJpbml0aWFsVmFsdWUiLCJzaG93TGFiZWxXaXRoVmFsdWUiLCJsYWJlbFZhbHVlU2VwYXJhdG9yIiwicmVzZXRDdXJzb3JPblVwZGF0ZSIsImVkaXRhYmxlUHJlZml4U2hvd24iLCJvIl0sInNvdXJjZXMiOlsiYmFzaFRvb2xVc2VPcHRpb25zLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBCQVNIX1RPT0xfTkFNRSB9IGZyb20gJy4uLy4uLy4uL3Rvb2xzL0Jhc2hUb29sL3Rvb2xOYW1lLmpzJ1xuaW1wb3J0IHsgZXh0cmFjdE91dHB1dFJlZGlyZWN0aW9ucyB9IGZyb20gJy4uLy4uLy4uL3V0aWxzL2Jhc2gvY29tbWFuZHMuanMnXG5pbXBvcnQgeyBpc0NsYXNzaWZpZXJQZXJtaXNzaW9uc0VuYWJsZWQgfSBmcm9tICcuLi8uLi8uLi91dGlscy9wZXJtaXNzaW9ucy9iYXNoQ2xhc3NpZmllci5qcydcbmltcG9ydCB0eXBlIHsgUGVybWlzc2lvbkRlY2lzaW9uUmVhc29uIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblJlc3VsdC5qcydcbmltcG9ydCB0eXBlIHsgUGVybWlzc2lvblVwZGF0ZSB9IGZyb20gJy4uLy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25VcGRhdGVTY2hlbWEuanMnXG5pbXBvcnQgeyBzaG91bGRTaG93QWx3YXlzQWxsb3dPcHRpb25zIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvcGVybWlzc2lvbnNMb2FkZXIuanMnXG5pbXBvcnQgdHlwZSB7IE9wdGlvbldpdGhEZXNjcmlwdGlvbiB9IGZyb20gJy4uLy4uL0N1c3RvbVNlbGVjdC9zZWxlY3QuanMnXG5pbXBvcnQgeyBnZW5lcmF0ZVNoZWxsU3VnZ2VzdGlvbnNMYWJlbCB9IGZyb20gJy4uL3NoZWxsUGVybWlzc2lvbkhlbHBlcnMuanMnXG5cbmV4cG9ydCB0eXBlIEJhc2hUb29sVXNlT3B0aW9uID1cbiAgfCAneWVzJ1xuICB8ICd5ZXMtYXBwbHktc3VnZ2VzdGlvbnMnXG4gIHwgJ3llcy1wcmVmaXgtZWRpdGVkJ1xuICB8ICd5ZXMtY2xhc3NpZmllci1yZXZpZXdlZCdcbiAgfCAnbm8nXG5cbi8qKlxuICogQ2hlY2sgaWYgYSBkZXNjcmlwdGlvbiBhbHJlYWR5IGV4aXN0cyBpbiB0aGUgYWxsb3cgbGlzdC5cbiAqIENvbXBhcmVzIGxvd2VyY2FzZSBhbmQgdHJhaWxpbmctd2hpdGVzcGFjZS10cmltbWVkIHZlcnNpb25zLlxuICovXG5mdW5jdGlvbiBkZXNjcmlwdGlvbkFscmVhZHlFeGlzdHMoXG4gIGRlc2NyaXB0aW9uOiBzdHJpbmcsXG4gIGV4aXN0aW5nRGVzY3JpcHRpb25zOiBzdHJpbmdbXSxcbik6IGJvb2xlYW4ge1xuICBjb25zdCBub3JtYWxpemVkID0gZGVzY3JpcHRpb24udG9Mb3dlckNhc2UoKS50cmltRW5kKClcbiAgcmV0dXJuIGV4aXN0aW5nRGVzY3JpcHRpb25zLnNvbWUoXG4gICAgZXhpc3RpbmcgPT4gZXhpc3RpbmcudG9Mb3dlckNhc2UoKS50cmltRW5kKCkgPT09IG5vcm1hbGl6ZWQsXG4gIClcbn1cblxuLyoqXG4gKiBTdHJpcCBvdXRwdXQgcmVkaXJlY3Rpb25zIHNvIGZpbGVuYW1lcyBkb24ndCBzaG93IGFzIGNvbW1hbmRzIGluIHRoZSBsYWJlbC5cbiAqL1xuZnVuY3Rpb24gc3RyaXBCYXNoUmVkaXJlY3Rpb25zKGNvbW1hbmQ6IHN0cmluZyk6IHN0cmluZyB7XG4gIGNvbnN0IHsgY29tbWFuZFdpdGhvdXRSZWRpcmVjdGlvbnMsIHJlZGlyZWN0aW9ucyB9ID1cbiAgICBleHRyYWN0T3V0cHV0UmVkaXJlY3Rpb25zKGNvbW1hbmQpXG4gIC8vIE9ubHkgdXNlIHN0cmlwcGVkIHZlcnNpb24gaWYgdGhlcmUgd2VyZSBhY3R1YWwgcmVkaXJlY3Rpb25zXG4gIHJldHVybiByZWRpcmVjdGlvbnMubGVuZ3RoID4gMCA/IGNvbW1hbmRXaXRob3V0UmVkaXJlY3Rpb25zIDogY29tbWFuZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gYmFzaFRvb2xVc2VPcHRpb25zKHtcbiAgc3VnZ2VzdGlvbnMgPSBbXSxcbiAgZGVjaXNpb25SZWFzb24sXG4gIG9uUmVqZWN0RmVlZGJhY2tDaGFuZ2UsXG4gIG9uQWNjZXB0RmVlZGJhY2tDaGFuZ2UsXG4gIG9uQ2xhc3NpZmllckRlc2NyaXB0aW9uQ2hhbmdlLFxuICBjbGFzc2lmaWVyRGVzY3JpcHRpb24sXG4gIGluaXRpYWxDbGFzc2lmaWVyRGVzY3JpcHRpb25FbXB0eSA9IGZhbHNlLFxuICBleGlzdGluZ0FsbG93RGVzY3JpcHRpb25zID0gW10sXG4gIHllc0lucHV0TW9kZSA9IGZhbHNlLFxuICBub0lucHV0TW9kZSA9IGZhbHNlLFxuICB
|