CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
ls.ts378 linesDownload Raw Back to tools
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import fs from 'node:fs/promises';8import path from 'node:path';9import type { ToolInvocation, ToolResult } from './tools.js';10import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';11import {12  makeRelative,13  shortenPath,14  unescapePath,15  isSubpaths,16  isSubpath,17} from '../utils/paths.js';18import type { Config } from '../config/config.js';19import type { PermissionDecision } from '../permissions/types.js';20import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';21import { ToolErrorType } from './tool-error.js';22import { ToolDisplayNames, ToolNames } from './tool-names.js';23import { createDebugLogger } from '../utils/debugLogger.js';24import { Storage } from '../config/storage.js';25import { getMemoryBaseDir } from '../memory/paths.js';26 27const debugLogger = createDebugLogger('LS');28 29const MAX_ENTRY_COUNT = 100;30 31/**32 * Parameters for the LS tool33 */34export interface LSToolParams {35  /**36   * The absolute path to the directory to list37   */38  path: string;39 40  /**41   * Array of glob patterns to ignore (optional)42   */43  ignore?: string[];44 45  /**46   * Whether to respect .gitignore and Qwen/agent ignore patterns (optional, defaults to true)47   */48  file_filtering_options?: {49    respect_git_ignore?: boolean;50    respect_qwen_ignore?: boolean;51  };52}53 54/**55 * File entry returned by LS tool56 */57export interface FileEntry {58  /**59   * Name of the file or directory60   */61  name: string;62 63  /**64   * Absolute path to the file or directory65   */66  path: string;67 68  /**69   * Whether this entry is a directory70   */71  isDirectory: boolean;72 73  /**74   * Size of the file in bytes (0 for directories)75   */76  size: number;77 78  /**79   * Last modified timestamp80   */81  modifiedTime: Date;82}83 84class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {85  constructor(86    private readonly config: Config,87    params: LSToolParams,88  ) {89    super(params);90  }91 92  /**93   * Checks if a filename matches any of the ignore patterns94   * @param filename Filename to check95   * @param patterns Array of glob patterns to check against96   * @returns True if the filename should be ignored97   */98  private shouldIgnore(filename: string, patterns?: string[]): boolean {99    if (!patterns || patterns.length === 0) {100      return false;101    }102    for (const pattern of patterns) {103      // Convert glob pattern to RegExp104      const regexPattern = pattern105        .replace(/[.+^${}()|[\]\\]/g, '\\$&')106        .replace(/\*/g, '.*')107        .replace(/\?/g, '.');108      const regex = new RegExp(`^${regexPattern}$`);109      if (regex.test(filename)) {110        return true;111      }112    }113    return false;114  }115 116  /**117   * Gets a description of the file reading operation118   * @returns A string describing the file being read119   */120  getDescription(): string {121    const relativePath = makeRelative(122      this.params.path,123      this.config.getTargetDir(),124    );125    return shortenPath(relativePath);126  }127 128  /**129   * Returns 'ask' for paths outside the workspace/userSkills directories,130   * so that external directory listings require user confirmation.131   */132  override async getDefaultPermission(): Promise<PermissionDecision> {133    const dirPath = path.resolve(this.params.path);134    const workspaceContext = this.config.getWorkspaceContext();135    const userSkillsDirs = this.config.storage.getUserSkillsDirs();136    const userExtensionsDir = Storage.getUserExtensionsDir();137 138    if (139      workspaceContext.isPathWithinWorkspace(dirPath) ||140      isSubpaths(userSkillsDirs, dirPath) ||141      isSubpath(userExtensionsDir, dirPath) ||142      isSubpath(getMemoryBaseDir(), dirPath)143    ) {144      return 'allow';145    }146    return 'ask';147  }148 149  // Helper for consistent error formatting150  private errorResult(151    llmContent: string,152    returnDisplay: string,153    type: ToolErrorType,154  ): ToolResult {155    return {156      llmContent,157      // Keep returnDisplay simpler in core logic158      returnDisplay: `Error: ${returnDisplay}`,159      error: {160        message: llmContent,161        type,162      },163    };164  }165 166  /**167   * Executes the LS operation with the given parameters168   * @returns Result of the LS operation169   */170  async execute(_signal: AbortSignal): Promise<ToolResult> {171    try {172      const stats = await fs.stat(this.params.path);173      if (!stats) {174        // fs.statSync throws on non-existence, so this check might be redundant175        // but keeping for clarity. Error message adjusted.176        return this.errorResult(177          `Error: Directory not found or inaccessible: ${this.params.path}`,178          `Directory not found or inaccessible.`,179          ToolErrorType.FILE_NOT_FOUND,180        );181      }182      if (!stats.isDirectory()) {183        return this.errorResult(184          `Error: Path is not a directory: ${this.params.path}`,185          `Path is not a directory.`,186          ToolErrorType.PATH_IS_NOT_A_DIRECTORY,187        );188      }189 190      const files = await fs.readdir(this.params.path);191      if (files.length === 0) {192        // Changed error message to be more neutral for LLM193        return {194          llmContent: `Directory ${this.params.path} is empty.`,195          returnDisplay: `Directory is empty.`,196        };197      }198 199      const relativePaths = files.map((file) =>200        path.relative(201          this.config.getTargetDir(),202          path.join(this.params.path, file),203        ),204      );205 206      const fileDiscovery = this.config.getFileService();207      const { filteredPaths, gitIgnoredCount, qwenIgnoredCount } =208        fileDiscovery.filterFilesWithReport(relativePaths, {209          respectGitIgnore:210            this.params.file_filtering_options?.respect_git_ignore ??211            this.config.getFileFilteringOptions().respectGitIgnore ??212            DEFAULT_FILE_FILTERING_OPTIONS.respectGitIgnore,213          respectQwenIgnore:214            this.params.file_filtering_options?.respect_qwen_ignore ??215            this.config.getFileFilteringOptions().respectQwenIgnore ??216            DEFAULT_FILE_FILTERING_OPTIONS.respectQwenIgnore,217        });218 219      const entries = [];220      for (const relativePath of filteredPaths) {221        const fullPath = path.resolve(this.config.getTargetDir(), relativePath);222 223        if (this.shouldIgnore(path.basename(fullPath), this.params.ignore)) {224          continue;225        }226 227        try {228          const stats = await fs.stat(fullPath);229          const isDir = stats.isDirectory();230          entries.push({231            name: path.basename(fullPath),232            path: fullPath,233            isDirectory: isDir,234            size: isDir ? 0 : stats.size,235            modifiedTime: stats.mtime,236          });237        } catch (error) {238          // Log error internally but don't fail the whole listing239          debugLogger.warn(`Error accessing ${fullPath}: ${error}`);240        }241      }242 243      // Sort entries (directories first, then alphabetically)244      entries.sort((a, b) => {245        if (a.isDirectory && !b.isDirectory) return -1;246        if (!a.isDirectory && b.isDirectory) return 1;247        return a.name.localeCompare(b.name);248      });249 250      const totalEntryCount = entries.length;251      const entryLimit = Math.min(252        MAX_ENTRY_COUNT,253        this.config.getTruncateToolOutputLines(),254      );255      const truncated = totalEntryCount > entryLimit;256 257      const entriesToShow = truncated ? entries.slice(0, entryLimit) : entries;258 259      const directoryContent = entriesToShow260        .map((entry) => `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`)261        .join('\n');262 263      let resultMessage = `Listed ${totalEntryCount} item(s) in ${this.params.path}:\n---\n${directoryContent}`;264 265      if (truncated) {266        const omittedEntries = totalEntryCount - entryLimit;267        const entryTerm = omittedEntries === 1 ? 'item' : 'items';268        resultMessage += `\n---\n[${omittedEntries} ${entryTerm} truncated] ...`;269      }270 271      const ignoredMessages = [];272      if (gitIgnoredCount > 0) {273        ignoredMessages.push(`${gitIgnoredCount} git-ignored`);274      }275      if (qwenIgnoredCount > 0) {276        ignoredMessages.push(`${qwenIgnoredCount} qwen-ignored`);277      }278      if (ignoredMessages.length > 0) {279        resultMessage += `\n\n(${ignoredMessages.join(', ')})`;280      }281 282      let displayMessage = `Listed ${totalEntryCount} item(s)`;283      if (ignoredMessages.length > 0) {284        displayMessage += ` (${ignoredMessages.join(', ')})`;285      }286      if (truncated) {287        displayMessage += ' (truncated)';288      }289 290      return {291        llmContent: resultMessage,292        returnDisplay: displayMessage,293      };294    } catch (error) {295      const errorMsg = `Error listing directory: ${error instanceof Error ? error.message : String(error)}`;296      return this.errorResult(297        errorMsg,298        'Failed to list directory.',299        ToolErrorType.LS_EXECUTION_ERROR,300      );301    }302  }303}304 305/**306 * Implementation of the LS tool logic307 */308export class LSTool extends BaseDeclarativeTool<LSToolParams, ToolResult> {309  static readonly Name = ToolNames.LS;310 311  constructor(private config: Config) {312    super(313      LSTool.Name,314      ToolDisplayNames.LS,315      'Lists the names of files and subdirectories directly within a specified directory path. Can optionally ignore entries matching provided glob patterns.',316      Kind.Search,317      {318        properties: {319          path: {320            description:321              'The absolute path to the directory to list (must be absolute, not relative)',322            type: 'string',323          },324          ignore: {325            description: 'List of glob patterns to ignore',326            items: {327              type: 'string',328            },329            type: 'array',330          },331          file_filtering_options: {332            description:333              'Optional: Whether to respect ignore patterns from .gitignore, .qwenignore, and configured custom Qwen ignore files',334            type: 'object',335            properties: {336              respect_git_ignore: {337                description:338                  'Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true.',339                type: 'boolean',340              },341              respect_qwen_ignore: {342                description:343                  'Optional: Whether to respect .qwenignore and configured custom Qwen ignore file patterns when listing files. Defaults to true.',344                type: 'boolean',345              },346            },347          },348        },349        required: ['path'],350        type: 'object',351      },352    );353  }354 355  /**356   * Validates the parameters for the tool357   * @param params Parameters to validate358   * @returns An error message string if invalid, null otherwise359   */360  protected override validateToolParamValues(361    params: LSToolParams,362  ): string | null {363    params.path = unescapePath(params.path.trim());364 365    if (!path.isAbsolute(params.path)) {366      return `Path must be absolute: ${params.path}`;367    }368 369    return null;370  }371 372  protected createInvocation(373    params: LSToolParams,374  ): ToolInvocation<LSToolParams, ToolResult> {375    return new LSToolInvocation(this.config, params);376  }377}378 
basant307/AI_Governance_Project · CoolFace