Skip to content

Commit c4450ce

Browse files
domdomeggclaude
andcommitted
feat(search_files): clarify description and standardise implementation
Previously description was confusing/ambigious about whether this was doing text content matching or filename matching. This clarifies it's by filename. Also the implementation for globs was confusing - it would sometimes use glob and sometimes substring match. This makes it all globs. Fixes #896 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fd886fa commit c4450ce

4 files changed

Lines changed: 32 additions & 35 deletions

File tree

src/filesystem/__tests__/directory-tree.test.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,10 @@ async function buildTreeForTesting(currentPath: string, rootPath: string, exclud
1919

2020
for (const entry of entries) {
2121
const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
22-
const shouldExclude = excludePatterns.some(pattern => {
23-
if (pattern.includes('*')) {
24-
return minimatch(relativePath, pattern, {dot: true});
25-
}
26-
// For files: match exact name or as part of path
27-
// For directories: match as directory path
28-
return minimatch(relativePath, pattern, {dot: true}) ||
29-
minimatch(relativePath, `**/${pattern}`, {dot: true}) ||
30-
minimatch(relativePath, `**/${pattern}/**`, {dot: true});
31-
});
22+
// Use glob matching exclusively for consistency
23+
const shouldExclude = excludePatterns.some(pattern =>
24+
minimatch(relativePath, pattern, {dot: true})
25+
);
3226
if (shouldExclude)
3327
continue;
3428

@@ -74,7 +68,7 @@ describe('buildTree exclude patterns', () => {
7468
});
7569

7670
it('should exclude files matching simple patterns', async () => {
77-
// Test the current implementation - this will fail until the bug is fixed
71+
// With strict glob matching, '.env' only matches exactly '.env'
7872
const tree = await buildTreeForTesting(testDir, testDir, ['.env']);
7973
const fileNames = tree.map(entry => entry.name);
8074

@@ -85,6 +79,7 @@ describe('buildTree exclude patterns', () => {
8579
});
8680

8781
it('should exclude directories matching simple patterns', async () => {
82+
// With strict glob matching, 'node_modules' only matches top-level
8883
const tree = await buildTreeForTesting(testDir, testDir, ['node_modules']);
8984
const dirNames = tree.map(entry => entry.name);
9085

@@ -93,8 +88,9 @@ describe('buildTree exclude patterns', () => {
9388
expect(dirNames).toContain('.git');
9489
});
9590

96-
it('should exclude nested directories with same pattern', async () => {
97-
const tree = await buildTreeForTesting(testDir, testDir, ['node_modules']);
91+
it('should exclude nested directories with glob pattern', async () => {
92+
// Use '**/node_modules' to match at any level
93+
const tree = await buildTreeForTesting(testDir, testDir, ['**/node_modules']);
9894

9995
// Find the nested directory
10096
const nestedDir = tree.find(entry => entry.name === 'nested');
@@ -124,7 +120,8 @@ describe('buildTree exclude patterns', () => {
124120
});
125121

126122
it('should work with multiple exclude patterns', async () => {
127-
const tree = await buildTreeForTesting(testDir, testDir, ['node_modules', '.env', '.git']);
123+
// Mix of exact matches and glob patterns
124+
const tree = await buildTreeForTesting(testDir, testDir, ['**/node_modules', '.env', '.git']);
128125
const entryNames = tree.map(entry => entry.name);
129126

130127
expect(entryNames).not.toContain('node_modules');

src/filesystem/__tests__/lib.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ describe('Lib Functions', () => {
316316

317317
const result = await searchFilesWithValidation(
318318
testDir,
319-
'*test*',
319+
['*test*'],
320320
allowedDirs,
321321
{ excludePatterns: ['*.log', 'node_modules'] }
322322
);
@@ -346,7 +346,7 @@ describe('Lib Functions', () => {
346346

347347
const result = await searchFilesWithValidation(
348348
testDir,
349-
'*test*',
349+
['*test*'],
350350
allowedDirs,
351351
{}
352352
);
@@ -370,7 +370,7 @@ describe('Lib Functions', () => {
370370

371371
const result = await searchFilesWithValidation(
372372
testDir,
373-
'*test*',
373+
['*test*'],
374374
allowedDirs,
375375
{ excludePatterns: ['*.backup'] }
376376
);

src/filesystem/index.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ const MoveFileArgsSchema = z.object({
132132

133133
const SearchFilesArgsSchema = z.object({
134134
path: z.string(),
135-
pattern: z.string(),
135+
patterns: z.array(z.string()),
136136
excludePatterns: z.array(z.string()).optional().default([])
137137
});
138138

@@ -261,6 +261,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
261261
"Get a recursive tree view of files and directories as a JSON structure. " +
262262
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
263263
"Files have no children array, while directories always have a children array (which may be empty). " +
264+
"Supports excluding paths using glob patterns (e.g., ['node_modules', '**/*.log', '.git']). " +
264265
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
265266
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
266267
},
@@ -276,9 +277,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
276277
{
277278
name: "search_files",
278279
description:
279-
"Recursively search for files and directories matching a pattern. " +
280-
"The patterns should be glob-style patterns that match paths relative to the working directory. " +
281-
"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
280+
"Recursively search for files and directories matching glob patterns. " +
281+
"The patterns should be glob-style patterns that match paths relative to the search path. " +
282+
"Use patterns like ['*.ext'] to match files in current directory, and ['**/*.ext'] to match files in all subdirectories. " +
283+
"Multiple patterns can be provided to match different file types, e.g., ['**/*.js', '**/*.ts']. " +
282284
"Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
283285
"Only searches within allowed directories.",
284286
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput,
@@ -539,16 +541,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
539541

540542
for (const entry of entries) {
541543
const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
542-
const shouldExclude = excludePatterns.some(pattern => {
543-
if (pattern.includes('*')) {
544-
return minimatch(relativePath, pattern, {dot: true});
545-
}
546-
// For files: match exact name or as part of path
547-
// For directories: match as directory path
548-
return minimatch(relativePath, pattern, {dot: true}) ||
549-
minimatch(relativePath, `**/${pattern}`, {dot: true}) ||
550-
minimatch(relativePath, `**/${pattern}/**`, {dot: true});
551-
});
544+
// Use glob matching exclusively for consistency
545+
const shouldExclude = excludePatterns.some(pattern =>
546+
minimatch(relativePath, pattern, {dot: true})
547+
);
552548
if (shouldExclude)
553549
continue;
554550

@@ -596,7 +592,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
596592
throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
597593
}
598594
const validPath = await validatePath(parsed.data.path);
599-
const results = await searchFilesWithValidation(validPath, parsed.data.pattern, allowedDirectories, { excludePatterns: parsed.data.excludePatterns });
595+
const results = await searchFilesWithValidation(validPath, parsed.data.patterns, allowedDirectories, { excludePatterns: parsed.data.excludePatterns });
600596
return {
601597
content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
602598
};

src/filesystem/lib.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ export async function headFile(filePath: string, numLines: number): Promise<stri
350350

351351
export async function searchFilesWithValidation(
352352
rootPath: string,
353-
pattern: string,
353+
patterns: string[],
354354
allowedDirectories: string[],
355355
options: SearchOptions = {}
356356
): Promise<string[]> {
@@ -373,8 +373,12 @@ export async function searchFilesWithValidation(
373373

374374
if (shouldExclude) continue;
375375

376-
// Use glob matching for the search pattern
377-
if (minimatch(relativePath, pattern, { dot: true })) {
376+
// Check if the path matches any of the patterns
377+
const matches = patterns.some(pattern =>
378+
minimatch(relativePath, pattern, { dot: true })
379+
);
380+
381+
if (matches) {
378382
results.push(fullPath);
379383
}
380384

0 commit comments

Comments
 (0)