UNPKG

@agentforce/adk

Version:

AgentForce Agent Development Kit - A powerful framework for building AI agents and servers

210 lines (189 loc) 8.63 kB
import { readdirSync, statSync } from "fs"; import { join, resolve, basename, relative } from "path"; import type { ToolImplementation } from "../types"; import { parseGitignore } from "../utils/gitignore_parser"; /** * Enhanced file system search tool - finds both files and directories matching a pattern * Includes .gitignore support and configurable exclusions */ export const fs_find_dirs_and_files: ToolImplementation = { definition: { type: "function", function: { name: "fs_find_dirs_and_files", description: "Find both files and directories matching a pattern, with .gitignore support and configurable exclusions", parameters: { type: "object", properties: { path: { type: "string", description: "The directory path to search in (defaults to current working directory). Can be relative or absolute.", }, pattern: { type: "string", description: "The pattern to match in file/directory names (case-insensitive substring match)", }, use_gitignore: { type: "boolean", description: "Whether to use .gitignore patterns for exclusions (default: true)", }, additional_excludes: { type: "array", description: "Additional directories/files to exclude from search", }, max_depth: { type: "number", description: "Maximum depth to search (default: 10, max: 20)", }, max_results: { type: "number", description: "Maximum total results to return (default: 100, max: 500)", }, case_sensitive: { type: "boolean", description: "Whether pattern matching should be case-sensitive (default: false)", }, }, required: ["pattern"], }, }, }, execute: async (args: Record<string, any>) => { const pattern = args.pattern; const searchPath = args.path || process.cwd(); const useGitignore = args.use_gitignore !== false; // Default to true const additionalExcludes = args.additional_excludes || []; const maxDepth = Math.min(args.max_depth || 10, 20); // Cap at 20 const maxResults = Math.min(args.max_results || 100, 500); // Cap at 500 const caseSensitive = args.case_sensitive === true; // Default to false try { // Resolve the absolute path const absoluteSearchPath = resolve(process.cwd(), searchPath); // Security check: ensure we're not searching outside the current working directory const cwd = resolve(process.cwd()); if (!absoluteSearchPath.startsWith(cwd)) { return { success: false, error: "Access denied: cannot search outside current working directory", path: searchPath, }; } // Check if the search path exists try { statSync(absoluteSearchPath); } catch { return { success: false, error: `Search path does not exist: ${searchPath}`, path: searchPath, }; } // Prepare exclusions let excludes: string[] = []; if (useGitignore) { excludes = parseGitignore(cwd); } excludes = [...excludes, ...additionalExcludes]; // Prepare pattern matching const searchPattern = caseSensitive ? pattern : pattern.toLowerCase(); const dirResults: string[] = []; const fileResults: string[] = []; const stack: Array<{ path: string; depth: number }> = [{ path: absoluteSearchPath, depth: 0 }]; let totalResults = 0; while (stack.length > 0 && totalResults < maxResults) { const { path: currentPath, depth } = stack.pop()!; // Check depth limit if (depth > maxDepth) { continue; } let stats; try { stats = statSync(currentPath); } catch { continue; // Skip inaccessible paths } const baseName = basename(currentPath); const relativePath = relative(cwd, currentPath); if (stats.isDirectory()) { // Check if directory should be excluded // Only check the basename for directory exclusions to avoid false positives if (useGitignore && excludes.includes(baseName)) { continue; } // Check additional excludes if (additionalExcludes.includes(baseName)) { continue; } // Check if directory name matches pattern const nameToCheck = caseSensitive ? baseName : baseName.toLowerCase(); if (nameToCheck.includes(searchPattern)) { dirResults.push(relativePath || "."); totalResults++; } // Add subdirectories to search stack try { const entries = readdirSync(currentPath); for (const entry of entries) { if (totalResults >= maxResults) break; stack.push({ path: join(currentPath, entry), depth: depth + 1, }); } } catch { continue; // Skip directories we can't read } } else if (stats.isFile()) { // Check if file should be excluded // Only check the basename for file exclusions to avoid false positives if (useGitignore && excludes.includes(baseName)) { continue; } // Check additional excludes if (additionalExcludes.includes(baseName)) { continue; } // Check if file name matches pattern const nameToCheck = caseSensitive ? baseName : baseName.toLowerCase(); if (nameToCheck.includes(searchPattern)) { fileResults.push(relativePath); totalResults++; } } } const totalFound = dirResults.length + fileResults.length; const truncated = totalResults >= maxResults; return { success: true, pattern: pattern, search_path: searchPath, absolute_search_path: absoluteSearchPath, case_sensitive: caseSensitive, max_depth: maxDepth, max_results: maxResults, use_gitignore: useGitignore, excludes: excludes.length, dirs: { count: dirResults.length, items: dirResults, }, files: { count: fileResults.length, items: fileResults, }, total_found: totalFound, truncated: truncated, message: truncated ? `Found ${totalFound} results (truncated at ${maxResults} limit). Use max_results parameter to increase limit.` : `Found ${totalFound} results (${dirResults.length} directories, ${fileResults.length} files)`, }; } catch (error: any) { return { success: false, error: error.message, pattern: pattern, path: searchPath, }; } }, };