Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/GrepTool.cs

135 lines
6.1 KiB
C#

using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace AxCopilot.Services.Agent;
/// <summary>파일 내용 텍스트 검색 도구. 정규식을 지원합니다.</summary>
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 => new()
{
Properties = new()
{
["pattern"] = new() { Type = "string", Description = "Search pattern (regex supported)" },
["path"] = new() { Type = "string", Description = "File or directory to search in. Optional, defaults to work folder." },
["glob"] = new() { Type = "string", Description = "File pattern filter (e.g. '*.cs', '*.json'). Optional." },
["context_lines"] = new() { Type = "integer", Description = "Number of context lines before/after each match (0-5). Default 0." },
["case_sensitive"] = new() { Type = "boolean", Description = "Case-sensitive search. Default false (case-insensitive)." },
},
Required = ["pattern"]
};
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
{
var pattern = args.GetProperty("pattern").GetString() ?? "";
var searchPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "";
var globFilter = args.TryGetProperty("glob", out var g) ? g.GetString() ?? "" : "";
var contextLines = args.TryGetProperty("context_lines", out var cl) ? Math.Clamp(cl.GetInt32(), 0, 5) : 0;
var caseSensitive = args.TryGetProperty("case_sensitive", out var cs) && cs.GetBoolean();
var baseDir = string.IsNullOrEmpty(searchPath)
? context.WorkFolder
: FileReadTool.ResolvePath(searchPath, context.WorkFolder);
if (string.IsNullOrEmpty(baseDir))
return Task.FromResult(ToolResult.Fail("작업 폴더가 설정되지 않았습니다."));
try
{
var regexOpts = RegexOptions.Compiled | (caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
var regex = new Regex(pattern, regexOpts, TimeSpan.FromSeconds(5));
var filePattern = string.IsNullOrEmpty(globFilter) ? "*" : globFilter;
IEnumerable<string> files;
if (File.Exists(baseDir))
files = [baseDir];
else if (Directory.Exists(baseDir))
files = Directory.EnumerateFiles(baseDir, filePattern, SearchOption.AllDirectories);
else
return Task.FromResult(ToolResult.Fail($"경로가 존재하지 않습니다: {baseDir}"));
var sb = new StringBuilder();
int matchCount = 0;
int fileCount = 0;
const int maxMatches = 100;
foreach (var file in files)
{
if (ct.IsCancellationRequested) break;
if (!context.IsPathAllowed(file)) continue;
if (IsBinaryFile(file)) continue;
try
{
var read = TextFileCodec.ReadAllText(file);
var lines = TextFileCodec.SplitLines(read.Text);
bool fileHit = false;
for (int i = 0; i < lines.Length && matchCount < maxMatches; i++)
{
if (regex.IsMatch(lines[i]))
{
if (!fileHit)
{
var rel = Directory.Exists(context.WorkFolder)
? Path.GetRelativePath(context.WorkFolder, file)
: file;
sb.AppendLine($"\n{rel}:");
fileHit = true;
fileCount++;
}
// 컨텍스트 라인 (before)
if (contextLines > 0)
{
for (int c = Math.Max(0, i - contextLines); c < i; c++)
sb.AppendLine($" {c + 1} {lines[c].TrimEnd()}");
}
sb.AppendLine($" {i + 1}: {lines[i].TrimEnd()}");
// 컨텍스트 라인 (after)
if (contextLines > 0)
{
for (int c = i + 1; c <= Math.Min(lines.Length - 1, i + contextLines); c++)
sb.AppendLine($" {c + 1} {lines[c].TrimEnd()}");
sb.AppendLine(" ---");
}
matchCount++;
}
}
}
catch { /* 읽기 실패 파일 무시 */ }
if (matchCount >= maxMatches) break;
}
if (matchCount == 0)
return Task.FromResult(ToolResult.Ok($"패턴 '{pattern}'에 일치하는 결과가 없습니다."));
var header = $"{fileCount}개 파일에서 {matchCount}개 일치{(matchCount >= maxMatches ? " ( )" : "")}:";
return Task.FromResult(ToolResult.Ok(header + sb));
}
catch (RegexParseException)
{
return Task.FromResult(ToolResult.Fail($"잘못된 정규식 패턴: {pattern}"));
}
catch (Exception ex)
{
return Task.FromResult(ToolResult.Fail($"검색 실패: {ex.Message}"));
}
}
private static bool IsBinaryFile(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
return ext is ".exe" or ".dll" or ".zip" or ".7z" or ".rar" or ".tar" or ".gz"
or ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" or ".webp"
or ".pdf" or ".docx" or ".xlsx" or ".pptx"
or ".mp3" or ".mp4" or ".avi" or ".mov" or ".mkv"
or ".psd" or ".msi" or ".iso" or ".bin" or ".dat" or ".db";
}
}