@@ -5,10 +5,38 @@ import type { ToolDefinition } from "./registry";
55import { resolveSafe , globToRegExp } from "./file-utils" ;
66import { appConfig } from "../config" ;
77
8+ /**
9+ * Directories that are always skipped during recursive traversal.
10+ * These are large build-artifact / dependency trees that have no value for
11+ * AI-driven code exploration and can easily produce millions of tokens.
12+ */
13+ const DEFAULT_EXCLUDE_DIRS = new Set ( [
14+ "node_modules" ,
15+ ".git" ,
16+ "dist" ,
17+ "build" ,
18+ "out" ,
19+ "target" , // Rust / Maven
20+ ".venv" , // Python virtualenv
21+ "venv" ,
22+ "__pycache__" ,
23+ ".cache" ,
24+ ".tox" ,
25+ "coverage" ,
26+ ".nyc_output" ,
27+ ] ) ;
28+
829const schema = z . object ( {
930 path : z . string ( ) . default ( "." ) . describe ( "Directory path relative to the workspace root (default: workspace root)" ) ,
1031 glob : z . string ( ) . optional ( ) . describe ( "Optional glob pattern to filter entries (e.g. '*.ts', '**/*.json')" ) ,
1132 recursive : z . boolean ( ) . optional ( ) . default ( false ) . describe ( "List entries recursively (default: false)" ) ,
33+ exclude : z
34+ . array ( z . string ( ) )
35+ . optional ( )
36+ . describe (
37+ "Additional directory names to exclude from recursive traversal " +
38+ "(node_modules, .git, dist, build, and other common build-artifact directories are always excluded)"
39+ ) ,
1240} ) ;
1341
1442/** A single directory entry returned by file-list. */
@@ -22,8 +50,13 @@ interface FileEntry {
2250
2351/**
2452 * Recursively collect entries under `dir`, returning paths relative to `baseDir`.
53+ * Directories whose base name appears in `excludeDirs` are silently skipped.
2554 */
26- async function collectEntries ( dir : string , baseDir : string ) : Promise < FileEntry [ ] > {
55+ async function collectEntries (
56+ dir : string ,
57+ baseDir : string ,
58+ excludeDirs : Set < string >
59+ ) : Promise < FileEntry [ ] > {
2760 const entries = await fs . readdir ( dir , { withFileTypes : true } ) ;
2861 const results : FileEntry [ ] = [ ] ;
2962
@@ -32,9 +65,11 @@ async function collectEntries(dir: string, baseDir: string): Promise<FileEntry[]
3265 const relativePath = path . relative ( baseDir , fullPath ) ;
3366
3467 if ( entry . isDirectory ( ) ) {
68+ // Skip excluded directories (e.g. node_modules, .git, dist)
69+ if ( excludeDirs . has ( entry . name ) ) continue ;
3570 results . push ( { path : relativePath , type : "directory" } ) ;
3671 // Recurse into sub-directory.
37- results . push ( ...( await collectEntries ( fullPath , baseDir ) ) ) ;
72+ results . push ( ...( await collectEntries ( fullPath , baseDir , excludeDirs ) ) ) ;
3873 } else if ( entry . isFile ( ) ) {
3974 const stat = await fs . stat ( fullPath ) ;
4075 results . push ( { path : relativePath , type : "file" , sizeBytes : stat . size } ) ;
@@ -55,17 +90,21 @@ export const toolDefinition: ToolDefinition = {
5590 path : dirPath = "." ,
5691 glob,
5792 recursive = false ,
93+ exclude = [ ] ,
5894 } : {
5995 path ?: string ;
6096 glob ?: string ;
6197 recursive ?: boolean ;
98+ exclude ?: string [ ] ;
6299 } ) : Promise < string > => {
63100 const resolved = resolveSafe ( appConfig . workspaceRoot , dirPath ) ;
64101
65102 let entries : FileEntry [ ] ;
66103
67104 if ( recursive ) {
68- entries = await collectEntries ( resolved , resolved ) ;
105+ // Merge caller-supplied exclusions with the built-in defaults
106+ const excludeDirs = new Set ( [ ...DEFAULT_EXCLUDE_DIRS , ...exclude ] ) ;
107+ entries = await collectEntries ( resolved , resolved , excludeDirs ) ;
69108 } else {
70109 const rawEntries = await fs . readdir ( resolved , { withFileTypes : true } ) ;
71110 entries = await Promise . all (
0 commit comments