mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 19:06:57 +10:00
177 lines
22 KiB
TypeScript
177 lines
22 KiB
TypeScript
|
|
import { homedir } from 'os';
|
||
|
|
import { basename, join, sep } from 'path';
|
||
|
|
import React, { type ReactNode } from 'react';
|
||
|
|
import { getOriginalCwd } from '../../../bootstrap/state.js';
|
||
|
|
import { Text } from '../../../ink.js';
|
||
|
|
import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js';
|
||
|
|
import type { ToolPermissionContext } from '../../../Tool.js';
|
||
|
|
import { expandPath, getDirectoryForPath } from '../../../utils/path.js';
|
||
|
|
import { normalizeCaseForComparison, pathInAllowedWorkingPath } from '../../../utils/permissions/filesystem.js';
|
||
|
|
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||
|
|
/**
|
||
|
|
* Check if a path is within the project's .claude/ folder.
|
||
|
|
* This is used to determine whether to show the special ".claude folder" permission option.
|
||
|
|
*/
|
||
|
|
export function isInClaudeFolder(filePath: string): boolean {
|
||
|
|
const absolutePath = expandPath(filePath);
|
||
|
|
const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`);
|
||
|
|
|
||
|
|
// Check if the path is within the project's .claude folder
|
||
|
|
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
|
||
|
|
const normalizedClaudeFolderPath = normalizeCaseForComparison(claudeFolderPath);
|
||
|
|
|
||
|
|
// Path must start with the .claude folder path (and be inside it, not just the folder itself)
|
||
|
|
return normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + sep.toLowerCase()) ||
|
||
|
|
// Also match case where sep is / on posix systems
|
||
|
|
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a path is within the global ~/.claude/ folder.
|
||
|
|
* This is used to determine whether to show the special ".claude folder" permission option
|
||
|
|
* for files in the user's home directory.
|
||
|
|
*/
|
||
|
|
export function isInGlobalClaudeFolder(filePath: string): boolean {
|
||
|
|
const absolutePath = expandPath(filePath);
|
||
|
|
const globalClaudeFolderPath = join(homedir(), '.claude');
|
||
|
|
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
|
||
|
|
const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(globalClaudeFolderPath);
|
||
|
|
return normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + sep.toLowerCase()) || normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/');
|
||
|
|
}
|
||
|
|
export type PermissionOption = {
|
||
|
|
type: 'accept-once';
|
||
|
|
} | {
|
||
|
|
type: 'accept-session';
|
||
|
|
scope?: 'claude-folder' | 'global-claude-folder';
|
||
|
|
} | {
|
||
|
|
type: 'reject';
|
||
|
|
};
|
||
|
|
export type PermissionOptionWithLabel = OptionWithDescription<string> & {
|
||
|
|
option: PermissionOption;
|
||
|
|
};
|
||
|
|
export type FileOperationType = 'read' | 'write' | 'create';
|
||
|
|
export function getFilePermissionOptions({
|
||
|
|
filePath,
|
||
|
|
toolPermissionContext,
|
||
|
|
operationType = 'write',
|
||
|
|
onRejectFeedbackChange,
|
||
|
|
onAcceptFeedbackChange,
|
||
|
|
yesInputMode = false,
|
||
|
|
noInputMode = false
|
||
|
|
}: {
|
||
|
|
filePath: string;
|
||
|
|
toolPermissionContext: ToolPermissionContext;
|
||
|
|
operationType?: FileOperationType;
|
||
|
|
onRejectFeedbackChange?: (value: string) => void;
|
||
|
|
onAcceptFeedbackChange?: (value: string) => void;
|
||
|
|
yesInputMode?: boolean;
|
||
|
|
noInputMode?: boolean;
|
||
|
|
}): PermissionOptionWithLabel[] {
|
||
|
|
const options: PermissionOptionWithLabel[] = [];
|
||
|
|
const modeCycleShortcut = getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab');
|
||
|
|
|
||
|
|
// When in input mode, show input field
|
||
|
|
if (yesInputMode && onAcceptFeedbackChange) {
|
||
|
|
options.push({
|
||
|
|
type: 'input',
|
||
|
|
label: 'Yes',
|
||
|
|
value: 'yes',
|
||
|
|
placeholder: 'and tell Claude what to do next',
|
||
|
|
onChange: onAcceptFeedbackChange,
|
||
|
|
allowEmptySubmitToCancel: true,
|
||
|
|
option: {
|
||
|
|
type: 'accept-once'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
options.push({
|
||
|
|
label: 'Yes',
|
||
|
|
value: 'yes',
|
||
|
|
option: {
|
||
|
|
type: 'accept-once'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const inAllowedPath = pathInAllowedWorkingPath(filePath, toolPermissionContext);
|
||
|
|
|
||
|
|
// Check if this is a .claude/ folder path (project or global)
|
||
|
|
const inClaudeFolder = isInClaudeFolder(filePath);
|
||
|
|
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath);
|
||
|
|
|
||
|
|
// Option 2: For .claude/ folder, show special option instead of generic session option
|
||
|
|
// Note: Session-level options are always shown since they only affect in-memory state,
|
||
|
|
// not persisted settings. The allowManagedPermissionRulesOnly setting only restricts
|
||
|
|
// persisted permission rules.
|
||
|
|
if ((inClaudeFolder || inGlobalClaudeFolder) && operationType !== 'read') {
|
||
|
|
options.push({
|
||
|
|
label: 'Yes, and allow Claude to edit its own settings for this session',
|
||
|
|
value: 'yes-claude-folder',
|
||
|
|
option: {
|
||
|
|
type: 'accept-session',
|
||
|
|
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// Option 2: Allow all changes/reads during session
|
||
|
|
let sessionLabel: ReactNode;
|
||
|
|
if (inAllowedPath) {
|
||
|
|
// Inside working directory
|
||
|
|
if (operationType === 'read') {
|
||
|
|
sessionLabel = 'Yes, during this session';
|
||
|
|
} else {
|
||
|
|
sessionLabel = <Text>
|
||
|
|
Yes, allow all edits during this session{' '}
|
||
|
|
<Text bold>({modeCycleShortcut})</Text>
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Outside working directory - include directory name
|
||
|
|
const dirPath = getDirectoryForPath(filePath);
|
||
|
|
const dirName = basename(dirPath) || 'this directory';
|
||
|
|
if (operationType === 'read') {
|
||
|
|
sessionLabel = <Text>
|
||
|
|
Yes, allow reading from <Text bold>{dirName}/</Text> during this
|
||
|
|
session
|
||
|
|
</Text>;
|
||
|
|
} else {
|
||
|
|
sessionLabel = <Text>
|
||
|
|
Yes, allow all edits in <Text bold>{dirName}/</Text> during this
|
||
|
|
session <Text bold>({modeCycleShortcut})</Text>
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
options.push({
|
||
|
|
label: sessionLabel,
|
||
|
|
value: 'yes-session',
|
||
|
|
option: {
|
||
|
|
type: 'accept-session'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// When in input mode, show input field for reject
|
||
|
|
if (noInputMode && onRejectFeedbackChange) {
|
||
|
|
options.push({
|
||
|
|
type: 'input',
|
||
|
|
label: 'No',
|
||
|
|
value: 'no',
|
||
|
|
placeholder: 'and tell Claude what to do differently',
|
||
|
|
onChange: onRejectFeedbackChange,
|
||
|
|
allowEmptySubmitToCancel: true,
|
||
|
|
option: {
|
||
|
|
type: 'reject'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// Not in input mode - simple option
|
||
|
|
options.push({
|
||
|
|
label: 'No',
|
||
|
|
value: 'no',
|
||
|
|
option: {
|
||
|
|
type: 'reject'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return options;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJob21lZGlyIiwiYmFzZW5hbWUiLCJqb2luIiwic2VwIiwiUmVhY3QiLCJSZWFjdE5vZGUiLCJnZXRPcmlnaW5hbEN3ZCIsIlRleHQiLCJnZXRTaG9ydGN1dERpc3BsYXkiLCJUb29sUGVybWlzc2lvbkNvbnRleHQiLCJleHBhbmRQYXRoIiwiZ2V0RGlyZWN0b3J5Rm9yUGF0aCIsIm5vcm1hbGl6ZUNhc2VGb3JDb21wYXJpc29uIiwicGF0aEluQWxsb3dlZFdvcmtpbmdQYXRoIiwiT3B0aW9uV2l0aERlc2NyaXB0aW9uIiwiaXNJbkNsYXVkZUZvbGRlciIsImZpbGVQYXRoIiwiYWJzb2x1dGVQYXRoIiwiY2xhdWRlRm9sZGVyUGF0aCIsIm5vcm1hbGl6ZWRBYnNvbHV0ZVBhdGgiLCJub3JtYWxpemVkQ2xhdWRlRm9sZGVyUGF0aCIsInN0YXJ0c1dpdGgiLCJ0b0xvd2VyQ2FzZSIsImlzSW5HbG9iYWxDbGF1ZGVGb2xkZXIiLCJnbG9iYWxDbGF1ZGVGb2xkZXJQYXRoIiwibm9ybWFsaXplZEdsb2JhbENsYXVkZUZvbGRlclBhdGgiLCJQZXJtaXNzaW9uT3B0aW9uIiwidHlwZSIsInNjb3BlIiwiUGVybWlzc2lvbk9wdGlvbldpdGhMYWJlbCIsIm9wdGlvbiIsIkZpbGVPcGVyYXRpb25UeXBlIiwiZ2V0RmlsZVBlcm1pc3Npb25PcHRpb25zIiwidG9vbFBlcm1pc3Npb25Db250ZXh0Iiwib3BlcmF0aW9uVHlwZSIsIm9uUmVqZWN0RmVlZGJhY2tDaGFuZ2UiLCJvbkFjY2VwdEZlZWRiYWNrQ2hhbmdlIiwieWVzSW5wdXRNb2RlIiwibm9JbnB1dE1vZGUiLCJ2YWx1ZSIsIm9wdGlvbnMiLCJtb2RlQ3ljbGVTaG9ydGN1dCIsInB1c2giLCJsYWJlbCIsInBsYWNlaG9sZGVyIiwib25DaGFuZ2UiLCJhbGxvd0VtcHR5U3VibWl0VG9DYW5jZWwiLCJpbkFsbG93ZWRQYXRoIiwiaW5DbGF1ZGVGb2xkZXIiLCJpbkdsb2JhbENsYXVkZUZvbGRlciIsInNlc3Npb25MYWJlbCIsImRpclBhdGgiLCJkaXJOYW1lIl0sInNvdXJjZXMiOlsicGVybWlzc2lvbk9wdGlvbnMudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGhvbWVkaXIgfSBmcm9tICdvcydcbmltcG9ydCB7IGJhc2VuYW1lLCBqb2luLCBzZXAgfSBmcm9tICdwYXRoJ1xuaW1wb3J0IFJlYWN0LCB7IHR5cGUgUmVhY3ROb2RlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBnZXRPcmlnaW5hbEN3ZCB9IGZyb20gJy4uLy4uLy4uL2Jvb3RzdHJhcC9zdGF0ZS5qcydcbmltcG9ydCB7IFRleHQgfSBmcm9tICcuLi8uLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBnZXRTaG9ydGN1dERpc3BsYXkgfSBmcm9tICcuLi8uLi8uLi9rZXliaW5kaW5ncy9zaG9ydGN1dEZvcm1hdC5qcydcbmltcG9ydCB0eXBlIHsgVG9vbFBlcm1pc3Npb25Db250ZXh0IH0gZnJvbSAnLi4vLi4vLi4vVG9vbC5qcydcbmltcG9ydCB7IGV4cGFuZFBhdGgsIGdldERpcmVjdG9yeUZvclBhdGggfSBmcm9tICcuLi8uLi8uLi91dGlscy9wYXRoLmpzJ1xuaW1wb3J0IHtcbiAgbm9ybWFsaXplQ2FzZUZvckNvbXBhcmlzb24sXG4gIHBhdGhJbkFsbG93ZWRXb3JraW5nUGF0aCxcbn0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvZmlsZXN5c3RlbS5qcydcbmltcG9ydCB0eXBlIHsgT3B0aW9uV2l0aERlc2NyaXB0aW9uIH0gZnJvbSAnLi4vLi4vQ3VzdG9tU2VsZWN0L3NlbGVjdC5qcydcbi8qKlxuICogQ2hlY2sgaWYgYSBwYXRoIGlzIHdpdGhpbiB0aGUgcHJvamVjdCdzIC5jbGF1ZGUvIGZvbGRlci5cbiAqIFRoaXMgaXMgdXNlZCB0byBkZXRlcm1pbmUgd2hldGhlciB0byBzaG93IHRoZSBzcGVjaWFsIFwiLmNsYXVkZSBmb2xkZXJcIiBwZXJtaXNzaW9uIG9wdGlvbi5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzSW5DbGF1ZGVGb2xkZXIoZmlsZVBhdGg6IHN0cmluZyk6IGJvb2xlYW4ge1xuICBjb25zdCBhYnNvbHV0ZVBhdGggPSBleHBhbmRQYXRoKGZpbGVQYXRoKVxuICBjb25zdCBjbGF1ZGVGb2xkZXJQYXRoID0gZXhwYW5kUGF0aChgJHtnZXRPcmlnaW5hbEN3ZCgpfS8uY2xhdWRlYClcblxuICAvLyBDaGVjayBpZiB0aGUgcGF0aCBpcyB3aXRoaW4gdGhlIHByb2plY3QncyAuY2xhdWRlIGZvbGRlclxuICBjb25zdCBub3JtYWxpemVkQWJzb2x1dGVQYXRoID0gbm9ybWFsaXplQ2FzZUZvckNvbXBhcmlzb24oYWJzb2x1dGVQYXRoKVxuICBjb25zdCBub3JtYWxpemVkQ2xhdWRlRm9sZGVyUGF0aCA9XG4gICAgbm9ybWFsaXplQ2FzZUZvckNvbXBhcmlzb24oY2xhdWRlRm9sZGVyUGF0aClcblxuICAvLyBQYXRoIG11c3Qgc3RhcnQgd2l0aCB0aGUgLmNsYXVkZSBmb2xkZXIgcGF0aCAoYW5kIGJlIGluc2lkZSBpdCwgbm90IGp1c3QgdGhlIGZvbGRlciBpdHNlbGYpXG4gIHJldHVybiAoXG4gICAgbm9ybWFsaXplZEFic29sdXRlUGF0aC5zdGFydHNXaXRoKFxuICAgICAgbm9ybWFsaXplZENsYXVkZUZvbGRlclBhdGggKyBzZXAudG9Mb3dlckNhc2UoKSxcbiAgICApIHx8XG4gICAgLy8gQWxzbyBtYXRjaCBjYXNlIHdoZXJlIHNlcCBpcyAvIG9uIHBvc2l4IHN5c3RlbXNcbiAgICBub3JtYWxpemVkQWJzb2x1dGVQYXRoLnN0YXJ0c1dpdGgobm9ybWFsaXplZENsYXVkZUZvbGRlclBhdGggKyAnLycpXG4gIClcbn1cblxuLyoqXG4gKiBDaGVjayBpZiBhIHBhdGggaXMgd2l0aGluIHRoZSBnbG9iYWwgfi8uY2xhdWRlLyBmb2xkZXIuXG4gKiBUaGlzIGlzIHVzZWQgdG8gZGV0ZXJtaW5lIHdoZXRoZXIgdG8gc2hvdyB0aGUgc3BlY2lhbCBcIi5jbGF1ZGUgZm9sZGVyXCIgcGVybWlzc2lvbiBvcHRpb25cbiAqIGZvciBmaWxlcyBpbiB0aGUgdXNlcidzIGhvbWUgZGlyZWN0b3J5LlxuICovXG5leHBvcnQgZnVuY3Rpb24gaXNJbkdsb2JhbENsYXVkZUZvbGRlcihmaWxlUGF0aDogc3RyaW5nKTogYm9vbGVhbiB7XG4gIGNvbnN0IGFic29sdXRlUGF0aCA9IGV4cGFuZFBhdGgoZmlsZVBhdGgpXG4gIGNvbnN0IGdsb2JhbENsYXVkZUZvbGRlclBhdGggPSBqb2luKGhvbWVkaXIoKSwgJy5jbGF1ZGUnKVxuXG4gIGNvbnN0IG5vcm1hbGl6ZWRBYnNvbHV0ZVBhdGggPSBub3JtYWxpemVDYXNlRm9yQ29tcGFyaXNvbihhYnNvbHV0ZVBhdGgpXG4gIGNvbnN0IG5vcm1hbGl6ZWRHbG9
|