fix(tools): Handle special characters in file paths for glob and read_many_files (#6507)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Gal Zahavi 2025-08-18 16:39:05 -07:00 committed by GitHub
parent fb3ceb0da4
commit 6fc68ff8d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 15 deletions

View File

@ -150,6 +150,34 @@ describe('GlobTool', () => {
expect(result.returnDisplay).toBe('No files found'); expect(result.returnDisplay).toBe('No files found');
}); });
it('should find files with special characters in the name', async () => {
await fs.writeFile(path.join(tempRootDir, 'file[1].txt'), 'content');
const params: GlobToolParams = { pattern: 'file[1].txt' };
const invocation = globTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 1 file(s)');
expect(result.llmContent).toContain(
path.join(tempRootDir, 'file[1].txt'),
);
});
it('should find files with special characters like [] and () in the path', async () => {
const filePath = path.join(
tempRootDir,
'src/app/[test]/(dashboard)/testing/components/code.tsx',
);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, 'content');
const params: GlobToolParams = {
pattern: 'src/app/[test]/(dashboard)/testing/components/code.tsx',
};
const invocation = globTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Found 1 file(s)');
expect(result.llmContent).toContain(filePath);
});
it('should correctly sort files by modification time (newest first)', async () => { it('should correctly sort files by modification time (newest first)', async () => {
const params: GlobToolParams = { pattern: '*.sortme' }; const params: GlobToolParams = { pattern: '*.sortme' };
const invocation = globTool.build(params); const invocation = globTool.build(params);

View File

@ -6,7 +6,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { glob } from 'glob'; import { glob, escape } from 'glob';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { import {
BaseDeclarativeTool, BaseDeclarativeTool,
@ -137,7 +137,13 @@ class GlobToolInvocation extends BaseToolInvocation<
let allEntries: GlobPath[] = []; let allEntries: GlobPath[] = [];
for (const searchDir of searchDirectories) { for (const searchDir of searchDirectories) {
const entries = (await glob(this.params.pattern, { let pattern = this.params.pattern;
const fullPath = path.join(searchDir, pattern);
if (fs.existsSync(fullPath)) {
pattern = escape(pattern);
}
const entries = (await glob(pattern, {
cwd: searchDir, cwd: searchDir,
withFileTypes: true, withFileTypes: true,
nodir: true, nodir: true,

View File

@ -527,6 +527,43 @@ describe('ReadManyFilesTool', () => {
expect(truncatedFileContent).toContain('L200'); expect(truncatedFileContent).toContain('L200');
expect(truncatedFileContent).not.toContain('L2400'); expect(truncatedFileContent).not.toContain('L2400');
}); });
it('should read files with special characters like [] and () in the path', async () => {
const filePath = 'src/app/[test]/(dashboard)/testing/components/code.tsx';
createFile(filePath, 'Content of receive-detail');
const params = { paths: [filePath] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const expectedPath = path.join(tempRootDir, filePath);
expect(result.llmContent).toEqual([
`--- ${expectedPath} ---
Content of receive-detail
`,
]);
expect(result.returnDisplay).toContain(
'Successfully read and concatenated content from **1 file(s)**',
);
});
it('should read files with special characters in the name', async () => {
createFile('file[1].txt', 'Content of file[1]');
const params = { paths: ['file[1].txt'] };
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const expectedPath = path.join(tempRootDir, 'file[1].txt');
expect(result.llmContent).toEqual([
`--- ${expectedPath} ---
Content of file[1]
`,
]);
expect(result.returnDisplay).toContain(
'Successfully read and concatenated content from **1 file(s)**',
);
});
}); });
describe('Batch Processing', () => { describe('Batch Processing', () => {

View File

@ -13,8 +13,9 @@ import {
} from './tools.js'; } from './tools.js';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { getErrorMessage } from '../utils/errors.js'; import { getErrorMessage } from '../utils/errors.js';
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { glob } from 'glob'; import { glob, escape } from 'glob';
import { getCurrentGeminiMdFilename } from './memoryTool.js'; import { getCurrentGeminiMdFilename } from './memoryTool.js';
import { import {
detectFileType, detectFileType,
@ -245,18 +246,27 @@ ${finalExclusionPatternsForDescription
const workspaceDirs = this.config.getWorkspaceContext().getDirectories(); const workspaceDirs = this.config.getWorkspaceContext().getDirectories();
for (const dir of workspaceDirs) { for (const dir of workspaceDirs) {
const entriesInDir = await glob( const processedPatterns = [];
searchPatterns.map((p) => p.replace(/\\/g, '/')), for (const p of searchPatterns) {
{ const normalizedP = p.replace(/\\/g, '/');
cwd: dir, const fullPath = path.join(dir, normalizedP);
ignore: effectiveExcludes, if (fs.existsSync(fullPath)) {
nodir: true, processedPatterns.push(escape(normalizedP));
dot: true, } else {
absolute: true, // The path does not exist or is not a file, so we treat it as a glob pattern.
nocase: true, processedPatterns.push(normalizedP);
signal, }
}, }
);
const entriesInDir = await glob(processedPatterns, {
cwd: dir,
ignore: effectiveExcludes,
nodir: true,
dot: true,
absolute: true,
nocase: true,
signal,
});
for (const entry of entriesInDir) { for (const entry of entriesInDir) {
allEntries.add(entry); allEntries.add(entry);
} }