diff options
| author | Evan Senter <[email protected]> | 2025-04-19 19:45:42 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-04-19 19:45:42 +0100 |
| commit | 3fce6cea27d3e6129d6c06e528b62e1b11bf7094 (patch) | |
| tree | 244b8e9ab94f902d65d4bda8739a6538e377ed17 /packages/server/src/tools/glob.ts | |
| parent | 0c9e1ef61be7db53e6e73b7208b649cd8cbed6c3 (diff) | |
Starting to modularize into separate cli / server packages. (#55)
* Starting to move a lot of code into packages/server
* More of the massive refactor, builds and runs, some issues though.
* Fixing outstanding issue with double messages.
* Fixing a minor UI issue.
* Fixing the build post-merge.
* Running formatting.
* Addressing comments.
Diffstat (limited to 'packages/server/src/tools/glob.ts')
| -rw-r--r-- | packages/server/src/tools/glob.ts | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/packages/server/src/tools/glob.ts b/packages/server/src/tools/glob.ts new file mode 100644 index 00000000..b9a3143c --- /dev/null +++ b/packages/server/src/tools/glob.ts @@ -0,0 +1,216 @@ +/** + * @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; +} + +/** + * Implementation of the Glob tool logic (moved from CLI) + */ +export class GlobLogic extends BaseTool<GlobToolParams, ToolResult> { + static readonly Name = 'glob'; // Keep static name + + /** + * The root directory that this tool is grounded in. + */ + private rootDirectory: string; + + /** + * Creates a new instance of the GlobLogic + * @param rootDirectory Root directory to ground this tool in. + */ + constructor(rootDirectory: string) { + super( + GlobLogic.Name, + '', // Display name handled by CLI wrapper + '', // Description handled by CLI wrapper + { + properties: { + pattern: { + description: + "The glob pattern to match against (e.g., '*.py', 'src/**/*.js', 'docs/*.md').", + type: 'string', + }, + path: { + description: + 'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.', + type: 'string', + }, + }, + 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 and 'path' (if provided) is a string."; + } + + 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}").`; + } + + try { + if (!fs.existsSync(searchDirAbsolute)) { + return `Search path does not exist: ${shortenPath(makeRelative(searchDirAbsolute, this.rootDirectory))} (absolute: ${searchDirAbsolute})`; + } + if (!fs.statSync(searchDirAbsolute).isDirectory()) { + return `Search path is not a directory: ${shortenPath(makeRelative(searchDirAbsolute, this.rootDirectory))} (absolute: ${searchDirAbsolute})`; + } + } 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): Promise<ToolResult> { + const validationError = this.validateToolParams(params); + if (validationError) { + return { + llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, + returnDisplay: `Error: Failed to execute tool.`, + }; + } + + 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, + ignore: ['**/node_modules/**', '**/.git/**'], + followSymbolicLinks: false, + suppressErrors: true, + }); + + if (!entries || entries.length === 0) { + const displayPath = makeRelative(searchDirAbsolute, this.rootDirectory); + return { + llmContent: `No files found matching pattern "${params.pattern}" within ${displayPath || '.'}.`, + 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 sortedRelativePaths = sortedAbsolutePaths.map((absPath) => + makeRelative(absPath, this.rootDirectory), + ); + + const fileListDescription = sortedRelativePaths.join('\n'); + const fileCount = sortedRelativePaths.length; + const relativeSearchDir = makeRelative( + searchDirAbsolute, + this.rootDirectory, + ); + const displayPath = shortenPath( + relativeSearchDir === '.' ? 'root directory' : relativeSearchDir, + ); + + return { + llmContent: `Found ${fileCount} file(s) matching "${params.pattern}" within ${displayPath}, 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.`, + }; + } + } +} |
