in packages/core/src/tools/read-many-files.ts [278:493]
async execute(
params: ReadManyFilesParams,
signal: AbortSignal,
): Promise<ToolResult> {
const validationError = this.validateParams(params);
if (validationError) {
return {
llmContent: `Error: Invalid parameters for ${this.displayName}. Reason: ${validationError}`,
returnDisplay: `## Parameter Error\n\n${validationError}`,
};
}
const {
paths: inputPatterns,
include = [],
exclude = [],
useDefaultExcludes = true,
respect_git_ignore = true,
} = params;
const respectGitIgnore =
respect_git_ignore ?? this.config.getFileFilteringRespectGitIgnore();
// Get centralized file discovery service
const fileDiscovery = this.config.getFileService();
const toolBaseDir = this.targetDir;
const filesToConsider = new Set<string>();
const skippedFiles: Array<{ path: string; reason: string }> = [];
const processedFilesRelativePaths: string[] = [];
const contentParts: PartListUnion = [];
const effectiveExcludes = useDefaultExcludes
? [...DEFAULT_EXCLUDES, ...exclude, ...this.geminiIgnorePatterns]
: [...exclude, ...this.geminiIgnorePatterns];
const searchPatterns = [...inputPatterns, ...include];
if (searchPatterns.length === 0) {
return {
llmContent: 'No search paths or include patterns provided.',
returnDisplay: `## Information\n\nNo search paths or include patterns were specified. Nothing to read or concatenate.`,
};
}
try {
const entries = await glob(searchPatterns, {
cwd: toolBaseDir,
ignore: effectiveExcludes,
nodir: true,
dot: true,
absolute: true,
nocase: true,
signal,
});
const filteredEntries = respectGitIgnore
? fileDiscovery
.filterFiles(
entries.map((p) => path.relative(toolBaseDir, p)),
{
respectGitIgnore,
},
)
.map((p) => path.resolve(toolBaseDir, p))
: entries;
let gitIgnoredCount = 0;
for (const absoluteFilePath of entries) {
// Security check: ensure the glob library didn't return something outside targetDir.
if (!absoluteFilePath.startsWith(toolBaseDir)) {
skippedFiles.push({
path: absoluteFilePath,
reason: `Security: Glob library returned path outside target directory. Base: ${toolBaseDir}, Path: ${absoluteFilePath}`,
});
continue;
}
// Check if this file was filtered out by git ignore
if (respectGitIgnore && !filteredEntries.includes(absoluteFilePath)) {
gitIgnoredCount++;
continue;
}
filesToConsider.add(absoluteFilePath);
}
// Add info about git-ignored files if any were filtered
if (gitIgnoredCount > 0) {
skippedFiles.push({
path: `${gitIgnoredCount} file(s)`,
reason: 'ignored',
});
}
} catch (error) {
return {
llmContent: `Error during file search: ${getErrorMessage(error)}`,
returnDisplay: `## File Search Error\n\nAn error occurred while searching for files:\n\`\`\`\n${getErrorMessage(error)}\n\`\`\``,
};
}
const sortedFiles = Array.from(filesToConsider).sort();
for (const filePath of sortedFiles) {
const relativePathForDisplay = path
.relative(toolBaseDir, filePath)
.replace(/\\/g, '/');
const fileType = detectFileType(filePath);
if (fileType === 'image' || fileType === 'pdf') {
const fileExtension = path.extname(filePath).toLowerCase();
const fileNameWithoutExtension = path.basename(filePath, fileExtension);
const requestedExplicitly = inputPatterns.some(
(pattern: string) =>
pattern.toLowerCase().includes(fileExtension) ||
pattern.includes(fileNameWithoutExtension),
);
if (!requestedExplicitly) {
skippedFiles.push({
path: relativePathForDisplay,
reason:
'asset file (image/pdf) was not explicitly requested by name or extension',
});
continue;
}
}
// Use processSingleFileContent for all file types now
const fileReadResult = await processSingleFileContent(
filePath,
toolBaseDir,
);
if (fileReadResult.error) {
skippedFiles.push({
path: relativePathForDisplay,
reason: `Read error: ${fileReadResult.error}`,
});
} else {
if (typeof fileReadResult.llmContent === 'string') {
const separator = DEFAULT_OUTPUT_SEPARATOR_FORMAT.replace(
'{filePath}',
relativePathForDisplay,
);
contentParts.push(`${separator}\n\n${fileReadResult.llmContent}\n\n`);
} else {
contentParts.push(fileReadResult.llmContent); // This is a Part for image/pdf
}
processedFilesRelativePaths.push(relativePathForDisplay);
const lines =
typeof fileReadResult.llmContent === 'string'
? fileReadResult.llmContent.split('\n').length
: undefined;
const mimetype = getSpecificMimeType(filePath);
recordFileOperationMetric(
this.config,
FileOperation.READ,
lines,
mimetype,
path.extname(filePath),
);
}
}
let displayMessage = `### ReadManyFiles Result (Target Dir: \`${this.targetDir}\`)\n\n`;
if (processedFilesRelativePaths.length > 0) {
displayMessage += `Successfully read and concatenated content from **${processedFilesRelativePaths.length} file(s)**.\n`;
if (processedFilesRelativePaths.length <= 10) {
displayMessage += `\n**Processed Files:**\n`;
processedFilesRelativePaths.forEach(
(p) => (displayMessage += `- \`${p}\`\n`),
);
} else {
displayMessage += `\n**Processed Files (first 10 shown):**\n`;
processedFilesRelativePaths
.slice(0, 10)
.forEach((p) => (displayMessage += `- \`${p}\`\n`));
displayMessage += `- ...and ${processedFilesRelativePaths.length - 10} more.\n`;
}
}
if (skippedFiles.length > 0) {
if (processedFilesRelativePaths.length === 0) {
displayMessage += `No files were read and concatenated based on the criteria.\n`;
}
if (skippedFiles.length <= 5) {
displayMessage += `\n**Skipped ${skippedFiles.length} item(s):**\n`;
} else {
displayMessage += `\n**Skipped ${skippedFiles.length} item(s) (first 5 shown):**\n`;
}
skippedFiles
.slice(0, 5)
.forEach(
(f) => (displayMessage += `- \`${f.path}\` (Reason: ${f.reason})\n`),
);
if (skippedFiles.length > 5) {
displayMessage += `- ...and ${skippedFiles.length - 5} more.\n`;
}
} else if (
processedFilesRelativePaths.length === 0 &&
skippedFiles.length === 0
) {
displayMessage += `No files were read and concatenated based on the criteria.\n`;
}
if (contentParts.length === 0) {
contentParts.push(
'No files matching the criteria were found or all were skipped.',
);
}
return {
llmContent: contentParts,
returnDisplay: displayMessage.trim(),
};
}