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/ls.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/ls.ts')
| -rw-r--r-- | packages/server/src/tools/ls.ts | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/packages/server/src/tools/ls.ts b/packages/server/src/tools/ls.ts new file mode 100644 index 00000000..0e856e80 --- /dev/null +++ b/packages/server/src/tools/ls.ts @@ -0,0 +1,276 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; +import { BaseTool, ToolResult } from './tools.js'; +import { SchemaValidator } from '../utils/schemaValidator.js'; +import { makeRelative, shortenPath } from '../utils/paths.js'; + +/** + * Parameters for the LS tool + */ +export interface LSToolParams { + /** + * The absolute path to the directory to list + */ + path: string; + + /** + * List of glob patterns to ignore + */ + ignore?: string[]; +} + +/** + * File entry returned by LS tool + */ +export interface FileEntry { + /** + * Name of the file or directory + */ + name: string; + + /** + * Absolute path to the file or directory + */ + path: string; + + /** + * Whether this entry is a directory + */ + isDirectory: boolean; + + /** + * Size of the file in bytes (0 for directories) + */ + size: number; + + /** + * Last modified timestamp + */ + modifiedTime: Date; +} + +/** + * Implementation of the LS tool logic + */ +export class LSLogic extends BaseTool<LSToolParams, ToolResult> { + static readonly Name = 'list_directory'; + + /** + * The root directory that this tool is grounded in. + * All path operations will be restricted to this directory. + */ + private rootDirectory: string; + + /** + * Creates a new instance of the LSLogic + * @param rootDirectory Root directory to ground this tool in. All operations will be restricted to this directory. + */ + constructor(rootDirectory: string) { + super( + LSLogic.Name, + '', // Display name handled by CLI wrapper + '', // Description handled by CLI wrapper + { + properties: { + path: { + description: + 'The absolute path to the directory to list (must be absolute, not relative)', + type: 'string', + }, + ignore: { + description: 'List of glob patterns to ignore', + items: { + type: 'string', + }, + type: 'array', + }, + }, + required: ['path'], + type: 'object', + }, + ); + + // Set the root directory + this.rootDirectory = path.resolve(rootDirectory); + } + + /** + * Checks if a path is within the root directory + * @param dirpath The path to check + * @returns True if the path is within the root directory, false otherwise + */ + private isWithinRoot(dirpath: string): boolean { + const normalizedPath = path.normalize(dirpath); + const normalizedRoot = path.normalize(this.rootDirectory); + // Ensure the normalizedRoot ends with a path separator for proper path comparison + const rootWithSep = normalizedRoot.endsWith(path.sep) + ? normalizedRoot + : normalizedRoot + path.sep; + return ( + normalizedPath === normalizedRoot || + normalizedPath.startsWith(rootWithSep) + ); + } + + /** + * Validates the parameters for the tool + * @param params Parameters to validate + * @returns An error message string if invalid, null otherwise + */ + validateToolParams(params: LSToolParams): string | null { + if ( + this.schema.parameters && + !SchemaValidator.validate( + this.schema.parameters as Record<string, unknown>, + params, + ) + ) { + return 'Parameters failed schema validation.'; + } + if (!path.isAbsolute(params.path)) { + return `Path must be absolute: ${params.path}`; + } + if (!this.isWithinRoot(params.path)) { + return `Path must be within the root directory (${this.rootDirectory}): ${params.path}`; + } + return null; + } + + /** + * Checks if a filename matches any of the ignore patterns + * @param filename Filename to check + * @param patterns Array of glob patterns to check against + * @returns True if the filename should be ignored + */ + private shouldIgnore(filename: string, patterns?: string[]): boolean { + if (!patterns || patterns.length === 0) { + return false; + } + for (const pattern of patterns) { + // Convert glob pattern to RegExp + const regexPattern = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + const regex = new RegExp(`^${regexPattern}$`); + if (regex.test(filename)) { + return true; + } + } + return false; + } + + /** + * Gets a description of the file reading operation + * @param params Parameters for the file reading + * @returns A string describing the file being read + */ + getDescription(params: LSToolParams): string { + const relativePath = makeRelative(params.path, this.rootDirectory); + return shortenPath(relativePath); + } + + // Helper for consistent error formatting + private errorResult(llmContent: string, returnDisplay: string): ToolResult { + return { + llmContent, + // Keep returnDisplay simpler in core logic + returnDisplay: `Error: ${returnDisplay}`, + }; + } + + /** + * Executes the LS operation with the given parameters + * @param params Parameters for the LS operation + * @returns Result of the LS operation + */ + async execute(params: LSToolParams): Promise<ToolResult> { + const validationError = this.validateToolParams(params); + if (validationError) { + return this.errorResult( + `Error: Invalid parameters provided. Reason: ${validationError}`, + `Failed to execute tool.`, + ); + } + + try { + const stats = fs.statSync(params.path); + if (!stats) { + // fs.statSync throws on non-existence, so this check might be redundant + // but keeping for clarity. Error message adjusted. + return this.errorResult( + `Error: Directory not found or inaccessible: ${params.path}`, + `Directory not found or inaccessible.`, + ); + } + if (!stats.isDirectory()) { + return this.errorResult( + `Error: Path is not a directory: ${params.path}`, + `Path is not a directory.`, + ); + } + + const files = fs.readdirSync(params.path); + const entries: FileEntry[] = []; + if (files.length === 0) { + // Changed error message to be more neutral for LLM + return { + llmContent: `Directory ${params.path} is empty.`, + returnDisplay: `Directory is empty.`, + }; + } + + for (const file of files) { + if (this.shouldIgnore(file, params.ignore)) { + continue; + } + + const fullPath = path.join(params.path, file); + try { + const stats = fs.statSync(fullPath); + const isDir = stats.isDirectory(); + entries.push({ + name: file, + path: fullPath, + isDirectory: isDir, + size: isDir ? 0 : stats.size, + modifiedTime: stats.mtime, + }); + } catch (error) { + // Log error internally but don't fail the whole listing + console.error(`Error accessing ${fullPath}: ${error}`); + } + } + + // Sort entries (directories first, then alphabetically) + entries.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + // Create formatted content for LLM + const directoryContent = entries + .map((entry) => { + // More concise format for LLM + return `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`; + }) + .join('\n'); + + return { + llmContent: `Directory listing for ${params.path}:\n${directoryContent}`, + // Simplified display, CLI wrapper can enhance + returnDisplay: `Listed ${entries.length} item(s).`, + }; + } catch (error) { + const errorMsg = `Error listing directory: ${error instanceof Error ? error.message : String(error)}`; + return this.errorResult(errorMsg, 'Failed to list directory.'); + } + } +} |
