summaryrefslogtreecommitdiff
path: root/packages/server/src/tools/ls.ts
diff options
context:
space:
mode:
authorEvan Senter <[email protected]>2025-04-19 19:45:42 +0100
committerGitHub <[email protected]>2025-04-19 19:45:42 +0100
commit3fce6cea27d3e6129d6c06e528b62e1b11bf7094 (patch)
tree244b8e9ab94f902d65d4bda8739a6538e377ed17 /packages/server/src/tools/ls.ts
parent0c9e1ef61be7db53e6e73b7208b649cd8cbed6c3 (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.ts276
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.');
+ }
+ }
+}