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
JavaScript
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