UNPKG

erosolar-cli

Version:

Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning

174 lines 7.42 kB
import { readdirSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { buildError } from '../core/errors.js'; /** * Creates the Glob tool for fast file pattern matching * * Features: * - Supports glob patterns like wildcard recursion or directory matching * - Returns matching file paths sorted by modification time (newest first) * - Ignores common directories (.git, node_modules, dist) * - Fast pattern matching optimized for large codebases * * @param workingDir - The working directory for pattern matching * @returns Array containing the Glob tool definition */ export function createGlobTools(workingDir) { return [ { name: 'Glob', description: 'Fast file pattern matching tool that works with any codebase size. Supports glob patterns like "**/*.js" or "src/**/*.ts". Returns matching file paths sorted by modification time.', parameters: { type: 'object', properties: { pattern: { type: 'string', description: 'The glob pattern to match files against (e.g., "**/*.ts", "src/**/*.js", "*.md"). Avoid overly broad patterns like "." or "*" - be specific.', }, path: { type: 'string', description: 'The directory to search in. If not specified, the current working directory will be used.', }, head_limit: { type: 'number', description: 'Maximum number of files to return. Defaults to 20. Use higher values only when necessary to reduce context usage.', }, }, required: ['pattern'], additionalProperties: false, }, handler: async (args) => { const pattern = args['pattern']; const pathArg = args['path']; const headLimit = typeof args['head_limit'] === 'number' ? args['head_limit'] : 20; // Default to 20 files // Validate pattern if (typeof pattern !== 'string' || !pattern.trim()) { return 'Error: pattern must be a non-empty string.'; } // Warn about overly broad patterns const trimmedPattern = pattern.trim(); if (trimmedPattern === '.' || trimmedPattern === '*' || trimmedPattern === '**/*') { return `Warning: Pattern "${pattern}" is too broad and will match many files. Please use a more specific pattern like "**/*.ts" or "src/**/*.js" to reduce context usage.`; } try { const searchPath = pathArg && typeof pathArg === 'string' ? resolveFilePath(workingDir, pathArg) : workingDir; // Perform glob search const matches = globSearch(searchPath, pattern); // Sort by modification time (newest first) const sorted = matches.sort((a, b) => { try { const statA = statSync(a); const statB = statSync(b); return statB.mtimeMs - statA.mtimeMs; } catch { return 0; } }); // Convert to relative paths const relativePaths = sorted.map(filePath => { const rel = relative(workingDir, filePath); return rel && !rel.startsWith('..') ? rel : filePath; }); if (relativePaths.length === 0) { return `No files found matching pattern: ${pattern}`; } // Apply head_limit const totalMatches = relativePaths.length; const limited = relativePaths.slice(0, headLimit); const truncated = totalMatches > headLimit; const summary = totalMatches === 1 ? '1 file found' : `${totalMatches} file${totalMatches === 1 ? '' : 's'} found`; let result = `${summary} matching "${pattern}":`; if (truncated) { result += ` (showing first ${headLimit} of ${totalMatches})`; } result += `\n\n${limited.join('\n')}`; if (truncated) { result += `\n\n... [${totalMatches - headLimit} more files truncated. Use head_limit parameter to see more]`; } return result; } catch (error) { return buildError('glob search', error, { pattern: String(pattern), path: pathArg ? String(pathArg) : undefined, }); } }, }, ]; } function resolveFilePath(workingDir, path) { const normalized = path.trim(); return normalized.startsWith('/') ? normalized : join(workingDir, normalized); } function globSearch(baseDir, pattern) { const results = []; const regex = globToRegex(pattern); const ignoredDirs = new Set([ '.git', 'node_modules', 'dist', '.next', 'build', 'coverage', '.turbo', '.cache', '__pycache__', '.pytest_cache', '.venv', 'venv', ]); function search(currentDir) { try { const entries = readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { if (ignoredDirs.has(entry.name)) { continue; } const fullPath = join(currentDir, entry.name); if (entry.isDirectory()) { search(fullPath); } else { // Test the full path against the pattern if (regex.test(fullPath)) { results.push(fullPath); } } } } catch (error) { // Silently ignore permission errors } } search(baseDir); return results; } function globToRegex(pattern) { // Escape special regex characters except glob wildcards let escaped = pattern .replace(/\./g, '\\.') .replace(/\+/g, '\\+') .replace(/\^/g, '\\^') .replace(/\$/g, '\\$') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/\[/g, '\\[') .replace(/\]/g, '\\]') .replace(/\{/g, '\\{') .replace(/\}/g, '\\}') .replace(/\|/g, '\\|'); // Convert glob patterns to regex escaped = escaped .replace(/\*\*/g, '<!GLOBSTAR!>') // Placeholder for ** .replace(/\*/g, '[^/]*') // * matches any characters except / .replace(/<!GLOBSTAR!>/g, '.*') // ** matches any characters including / .replace(/\?/g, '.'); // ? matches any single character return new RegExp(`${escaped}$`); } //# sourceMappingURL=globTools.js.map