diff options
| author | Tommaso Sciortino <[email protected]> | 2025-05-30 18:25:47 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-05-30 18:25:47 -0700 |
| commit | 21fba832d1b4ea7af43fb887d9b2b38fcf8210d0 (patch) | |
| tree | 7200d2fac3a55c385e0a2dac34b5282c942364bc /packages/core/src/tools/glob.ts | |
| parent | c81148a0cc8489f657901c2cc7247c0834075e1a (diff) | |
Rename server->core (#638)
Diffstat (limited to 'packages/core/src/tools/glob.ts')
| -rw-r--r-- | packages/core/src/tools/glob.ts | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts new file mode 100644 index 00000000..86aef44f --- /dev/null +++ b/packages/core/src/tools/glob.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; +import fg from 'fast-glob'; +import { SchemaValidator } from '../utils/schemaValidator.js'; +import { BaseTool, ToolResult } from './tools.js'; +import { shortenPath, makeRelative } from '../utils/paths.js'; + +/** + * Parameters for the GlobTool + */ +export interface GlobToolParams { + /** + * The glob pattern to match files against + */ + pattern: string; + + /** + * The directory to search in (optional, defaults to current directory) + */ + path?: string; + + /** + * Whether the search should be case-sensitive (optional, defaults to false) + */ + case_sensitive?: boolean; +} + +/** + * Implementation of the Glob tool logic + */ +export class GlobTool extends BaseTool<GlobToolParams, ToolResult> { + static readonly Name = 'glob'; + /** + * Creates a new instance of the GlobLogic + * @param rootDirectory Root directory to ground this tool in. + */ + constructor(private rootDirectory: string) { + super( + GlobTool.Name, + 'FindFiles', + 'Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.', + { + properties: { + pattern: { + description: + "The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').", + type: 'string', + }, + path: { + description: + 'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.', + type: 'string', + }, + case_sensitive: { + description: + 'Optional: Whether the search should be case-sensitive. Defaults to false.', + type: 'boolean', + }, + }, + required: ['pattern'], + type: 'object', + }, + ); + + this.rootDirectory = path.resolve(rootDirectory); + } + + /** + * Checks if a path is within the root directory. + */ + private isWithinRoot(pathToCheck: string): boolean { + const absolutePathToCheck = path.resolve(pathToCheck); + const normalizedPath = path.normalize(absolutePathToCheck); + const normalizedRoot = path.normalize(this.rootDirectory); + const rootWithSep = normalizedRoot.endsWith(path.sep) + ? normalizedRoot + : normalizedRoot + path.sep; + return ( + normalizedPath === normalizedRoot || + normalizedPath.startsWith(rootWithSep) + ); + } + + /** + * Validates the parameters for the tool. + */ + validateToolParams(params: GlobToolParams): string | null { + if ( + this.schema.parameters && + !SchemaValidator.validate( + this.schema.parameters as Record<string, unknown>, + params, + ) + ) { + return "Parameters failed schema validation. Ensure 'pattern' is a string, 'path' (if provided) is a string, and 'case_sensitive' (if provided) is a boolean."; + } + + const searchDirAbsolute = path.resolve( + this.rootDirectory, + params.path || '.', + ); + + if (!this.isWithinRoot(searchDirAbsolute)) { + return `Search path ("${searchDirAbsolute}") resolves outside the tool's root directory ("${this.rootDirectory}").`; + } + + const targetDir = searchDirAbsolute || this.rootDirectory; + try { + if (!fs.existsSync(targetDir)) { + return `Search path does not exist ${targetDir}`; + } + if (!fs.statSync(targetDir).isDirectory()) { + return `Search path is not a directory: ${targetDir}`; + } + } catch (e: unknown) { + return `Error accessing search path: ${e}`; + } + + if ( + !params.pattern || + typeof params.pattern !== 'string' || + params.pattern.trim() === '' + ) { + return "The 'pattern' parameter cannot be empty."; + } + + return null; + } + + /** + * Gets a description of the glob operation. + */ + getDescription(params: GlobToolParams): string { + let description = `'${params.pattern}'`; + if (params.path) { + const searchDir = path.resolve(this.rootDirectory, params.path || '.'); + const relativePath = makeRelative(searchDir, this.rootDirectory); + description += ` within ${shortenPath(relativePath)}`; + } + return description; + } + + /** + * Executes the glob search with the given parameters + */ + async execute( + params: GlobToolParams, + _signal: AbortSignal, + ): Promise<ToolResult> { + const validationError = this.validateToolParams(params); + if (validationError) { + return { + llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, + returnDisplay: validationError, + }; + } + + try { + const searchDirAbsolute = path.resolve( + this.rootDirectory, + params.path || '.', + ); + + const entries = await fg(params.pattern, { + cwd: searchDirAbsolute, + absolute: true, + onlyFiles: true, + stats: true, + dot: true, + caseSensitiveMatch: params.case_sensitive ?? false, + ignore: ['**/node_modules/**', '**/.git/**'], + followSymbolicLinks: false, + suppressErrors: true, + }); + + if (!entries || entries.length === 0) { + return { + llmContent: `No files found matching pattern "${params.pattern}" within ${searchDirAbsolute}.`, + returnDisplay: `No files found`, + }; + } + + entries.sort((a, b) => { + const mtimeA = a.stats?.mtime?.getTime() ?? 0; + const mtimeB = b.stats?.mtime?.getTime() ?? 0; + return mtimeB - mtimeA; + }); + + const sortedAbsolutePaths = entries.map((entry) => entry.path); + const fileListDescription = sortedAbsolutePaths.join('\n'); + const fileCount = sortedAbsolutePaths.length; + + return { + llmContent: `Found ${fileCount} file(s) matching "${params.pattern}" within ${searchDirAbsolute}, sorted by modification time (newest first):\n${fileListDescription}`, + returnDisplay: `Found ${fileCount} matching file(s)`, + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error(`GlobLogic execute Error: ${errorMessage}`, error); + return { + llmContent: `Error during glob search operation: ${errorMessage}`, + returnDisplay: `Error: An unexpected error occurred.`, + }; + } + } +} |
