The-Agency/lib/src/tools/glob_tool.dart

146 lines
3.9 KiB
Dart

import "dart:io";
import "base_tool.dart";
class GlobTool extends BaseTool {
@override
final String name = "Glob";
@override
final String description =
"Fast file pattern matching. Supports glob patterns like \"**/*.js\". "
"Returns matching file paths sorted by modification time.";
@override
Future<String> execute(Map<String, dynamic> input) async {
final pattern = requireString(input, "pattern");
final pathArg = optionalString(input, "path");
final searchDir = pathArg != null ? Directory(pathArg) : Directory.current;
if (!await searchDir.exists()) {
return "Error: Directory does not exist: ${searchDir.path}";
}
final start = DateTime.now();
final matched = <FileSystemEntity>[];
await for (final entity in searchDir.list(recursive: true, followLinks: false)) {
if (entity is File) {
final rel = entity.path.substring(searchDir.path.length);
// strip leading slash
final relClean = rel.startsWith("/") ? rel.substring(1) : rel;
if (_matchGlob(pattern, relClean)) {
matched.add(entity);
}
}
}
const maxResults = 100;
final truncated = matched.length > maxResults;
final limited = truncated ? matched.sublist(0, maxResults) : matched;
// sort by mtime desc (same as original)
final withStat = <MapEntry<FileSystemEntity, DateTime>>[];
for (final f in limited) {
try {
final st = await f.stat();
withStat.add(MapEntry(f, st.modified));
} catch (_) {
withStat.add(MapEntry(f, DateTime.fromMillisecondsSinceEpoch(0)));
}
}
withStat.sort((a, b) => b.value.compareTo(a.value));
final filenames = withStat.map((e) {
final rel = e.key.path.substring(searchDir.path.length);
return rel.startsWith("/") ? rel.substring(1) : rel;
}).toList();
final durationMs = DateTime.now().difference(start).inMilliseconds;
if (filenames.isEmpty) {
return "No files found";
}
final buf = StringBuffer();
for (final f in filenames) {
buf.writeln(f);
}
if (truncated) {
buf.writeln("(Results are truncated. Consider using a more specific path or pattern.)");
}
// small summary footer
buf.write("Found ${filenames.length} file${filenames.length == 1 ? "" : "s"} in ${durationMs}ms");
return buf.toString();
}
// basic glob matching - handles ** and *
bool _matchGlob(String pattern, String path) {
return _globMatch(pattern, path);
}
bool _globMatch(String pattern, String text) {
// convert glob to regex-ish check
final regexStr = _globToRegex(pattern);
final regex = RegExp(regexStr);
return regex.hasMatch(text);
}
String _globToRegex(String pattern) {
final buf = StringBuffer("^");
int i = 0;
while (i < pattern.length) {
final c = pattern[i];
if (c == "*") {
if (i + 1 < pattern.length && pattern[i + 1] == "*") {
// ** matches anything including slashes
buf.write(".*");
i += 2;
// skip optional trailing slash after **
if (i < pattern.length && pattern[i] == "/") i++;
} else {
// * matches anything except slash
buf.write("[^/]*");
i++;
}
} else if (c == "?") {
buf.write("[^/]");
i++;
} else if (c == ".") {
buf.write("\\.");
i++;
} else if (c == "{") {
// {a,b,c} style alternation
final close = pattern.indexOf("}", i);
if (close == -1) {
buf.write("\\{");
i++;
} else {
final inner = pattern.substring(i + 1, close);
final parts = inner.split(",").map(_globToRegex).join("|");
buf.write("(?:$parts)");
i = close + 1;
}
} else if ("[]()|^".contains(c)) {
buf.write("\\$c");
i++;
} else {
buf.write(c);
i++;
}
}
buf.write(r"$");
return buf.toString();
}
}