using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class GrepTool : IAgentTool { public string Name => "grep"; public string Description => "Search file contents for a pattern (regex supported). Returns matching lines with file paths and line numbers."; public ToolParameterSchema Parameters { get { ToolParameterSchema obj = new ToolParameterSchema { Properties = new Dictionary { ["pattern"] = new ToolProperty { Type = "string", Description = "Search pattern (regex supported)" }, ["path"] = new ToolProperty { Type = "string", Description = "File or directory to search in. Optional, defaults to work folder." }, ["glob"] = new ToolProperty { Type = "string", Description = "File pattern filter (e.g. '*.cs', '*.json'). Optional." }, ["context_lines"] = new ToolProperty { Type = "integer", Description = "Number of context lines before/after each match (0-5). Default 0." }, ["case_sensitive"] = new ToolProperty { Type = "boolean", Description = "Case-sensitive search. Default false (case-insensitive)." } } }; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); CollectionsMarshal.AsSpan(list)[0] = "pattern"; obj.Required = list; return obj; } } public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { string text = args.GetProperty("pattern").GetString() ?? ""; JsonElement value; string text2 = (args.TryGetProperty("path", out value) ? (value.GetString() ?? "") : ""); JsonElement value2; string text3 = (args.TryGetProperty("glob", out value2) ? (value2.GetString() ?? "") : ""); JsonElement value3; int num = (args.TryGetProperty("context_lines", out value3) ? Math.Clamp(value3.GetInt32(), 0, 5) : 0); JsonElement value4; bool flag = args.TryGetProperty("case_sensitive", out value4) && value4.GetBoolean(); string text4 = (string.IsNullOrEmpty(text2) ? context.WorkFolder : FileReadTool.ResolvePath(text2, context.WorkFolder)); if (string.IsNullOrEmpty(text4)) { return Task.FromResult(ToolResult.Fail("작업 폴더가 설정되지 않았습니다.")); } try { RegexOptions options = (RegexOptions)(8 | ((!flag) ? 1 : 0)); Regex regex = new Regex(text, options, TimeSpan.FromSeconds(5.0)); string searchPattern = (string.IsNullOrEmpty(text3) ? "*" : text3); IEnumerable enumerable; if (File.Exists(text4)) { enumerable = new _003C_003Ez__ReadOnlySingleElementList(text4); } else { if (!Directory.Exists(text4)) { return Task.FromResult(ToolResult.Fail("경로가 존재하지 않습니다: " + text4)); } enumerable = Directory.EnumerateFiles(text4, searchPattern, SearchOption.AllDirectories); } StringBuilder stringBuilder = new StringBuilder(); int num2 = 0; int num3 = 0; foreach (string item in enumerable) { if (ct.IsCancellationRequested) { break; } if (!context.IsPathAllowed(item) || IsBinaryFile(item)) { continue; } try { string[] array = File.ReadAllLines(item, Encoding.UTF8); bool flag2 = false; for (int i = 0; i < array.Length; i++) { if (num2 >= 100) { break; } if (!regex.IsMatch(array[i])) { continue; } StringBuilder stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler; if (!flag2) { string value5 = (Directory.Exists(context.WorkFolder) ? Path.GetRelativePath(context.WorkFolder, item) : item); stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); handler.AppendLiteral("\n"); handler.AppendFormatted(value5); handler.AppendLiteral(":"); stringBuilder3.AppendLine(ref handler); flag2 = true; num3++; } if (num > 0) { for (int j = Math.Max(0, i - num); j < i; j++) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(j + 1); handler.AppendLiteral(" "); handler.AppendFormatted(array[j].TrimEnd()); stringBuilder4.AppendLine(ref handler); } } stringBuilder2 = stringBuilder; StringBuilder stringBuilder5 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(i + 1); handler.AppendLiteral(": "); handler.AppendFormatted(array[i].TrimEnd()); stringBuilder5.AppendLine(ref handler); if (num > 0) { for (int k = i + 1; k <= Math.Min(array.Length - 1, i + num); k++) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder6 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(k + 1); handler.AppendLiteral(" "); handler.AppendFormatted(array[k].TrimEnd()); stringBuilder6.AppendLine(ref handler); } stringBuilder.AppendLine(" ---"); } num2++; } } catch { } if (num2 < 100) { continue; } break; } if (num2 == 0) { return Task.FromResult(ToolResult.Ok("패턴 '" + text + "'에 일치하는 결과가 없습니다.")); } string text5 = $"{num3}개 파일에서 {num2}개 일치{((num2 >= 100) ? " (제한 도달)" : "")}:"; return Task.FromResult(ToolResult.Ok(text5 + stringBuilder)); } catch (RegexParseException) { return Task.FromResult(ToolResult.Fail("잘못된 정규식 패턴: " + text)); } catch (Exception ex2) { return Task.FromResult(ToolResult.Fail("검색 실패: " + ex2.Message)); } } private static bool IsBinaryFile(string path) { switch (Path.GetExtension(path).ToLowerInvariant()) { case ".exe": case ".dll": case ".zip": case ".7z": case ".rar": case ".tar": case ".gz": case ".png": case ".jpg": case ".jpeg": case ".gif": case ".bmp": case ".ico": case ".webp": case ".pdf": case ".docx": case ".xlsx": case ".pptx": case ".mp3": case ".mp4": case ".avi": case ".mov": case ".mkv": case ".psd": case ".msi": case ".iso": case ".bin": case ".dat": case ".db": return true; default: return false; } } }