using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class FileEditTool : IAgentTool { public string Name => "file_edit"; public string Description => "Edit a file by replacing an exact string match. Set replace_all=true to replace all occurrences; otherwise old_string must be unique."; public ToolParameterSchema Parameters { get { ToolParameterSchema obj = new ToolParameterSchema { Properties = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "File path to edit" }, ["old_string"] = new ToolProperty { Type = "string", Description = "Exact string to find and replace" }, ["new_string"] = new ToolProperty { Type = "string", Description = "Replacement string" }, ["replace_all"] = new ToolProperty { Type = "boolean", Description = "Replace all occurrences (default false). If false, old_string must be unique." } } }; int num = 3; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "path"; span[1] = "old_string"; span[2] = "new_string"; obj.Required = list; return obj; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { string path = args.GetProperty("path").GetString() ?? ""; string oldStr = args.GetProperty("old_string").GetString() ?? ""; string newStr = args.GetProperty("new_string").GetString() ?? ""; JsonElement ra; bool replaceAll = args.TryGetProperty("replace_all", out ra) && ra.GetBoolean(); string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (!context.IsPathAllowed(fullPath)) { return ToolResult.Fail("경로 접근 차단: " + fullPath); } if (!File.Exists(fullPath)) { return ToolResult.Fail("파일이 존재하지 않습니다: " + fullPath); } if (!(await context.CheckWritePermissionAsync(Name, fullPath))) { return ToolResult.Fail("쓰기 권한 거부: " + fullPath); } try { string content = await File.ReadAllTextAsync(fullPath, Encoding.UTF8, ct); int count = CountOccurrences(content, oldStr); if (count == 0) { return ToolResult.Fail("old_string을 파일에서 찾을 수 없습니다."); } if (!replaceAll && count > 1) { return ToolResult.Fail($"old_string이 {count}번 발견됩니다. replace_all=true로 전체 교체하거나, 고유한 문자열을 지정하세요."); } string diffPreview = GenerateDiff(content, oldStr, newStr, fullPath); string updated = content.Replace(oldStr, newStr); await File.WriteAllTextAsync(fullPath, updated, Encoding.UTF8, ct); string msg = ((replaceAll && count > 1) ? $"파일 수정 완료: {fullPath} ({count}곳 전체 교체)" : ("파일 수정 완료: " + fullPath)); return ToolResult.Ok(msg + "\n\n" + diffPreview, fullPath); } catch (Exception ex) { return ToolResult.Fail("파일 수정 실패: " + ex.Message); } } private static string GenerateDiff(string content, string oldStr, string newStr, string filePath) { string[] array = content.Split('\n'); int num = content.IndexOf(oldStr, StringComparison.Ordinal); if (num < 0) { return ""; } int num2 = content.Substring(0, num).Count((char c) => c == '\n'); string[] array2 = oldStr.Split('\n'); string[] array3 = newStr.Split('\n'); StringBuilder stringBuilder = new StringBuilder(); string fileName = Path.GetFileName(filePath); StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder2); handler.AppendLiteral("--- "); handler.AppendFormatted(fileName); handler.AppendLiteral(" (before)"); stringBuilder3.AppendLine(ref handler); stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(12, 1, stringBuilder2); handler.AppendLiteral("+++ "); handler.AppendFormatted(fileName); handler.AppendLiteral(" (after)"); stringBuilder4.AppendLine(ref handler); int num3 = Math.Max(0, num2 - 2); int num4 = Math.Min(array.Length - 1, num2 + array2.Length - 1 + 2); stringBuilder2 = stringBuilder; StringBuilder stringBuilder5 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(8, 2, stringBuilder2); handler.AppendLiteral("@@ -"); handler.AppendFormatted(num3 + 1); handler.AppendLiteral(","); handler.AppendFormatted(num4 - num3 + 1); handler.AppendLiteral(" @@"); stringBuilder5.AppendLine(ref handler); for (int num5 = num3; num5 < num2; num5++) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder6 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(array[num5].TrimEnd('\r')); stringBuilder6.AppendLine(ref handler); } string[] array4 = array2; foreach (string text in array4) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder7 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendLiteral("-"); handler.AppendFormatted(text.TrimEnd('\r')); stringBuilder7.AppendLine(ref handler); } string[] array5 = array3; foreach (string text2 in array5) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder8 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendLiteral("+"); handler.AppendFormatted(text2.TrimEnd('\r')); stringBuilder8.AppendLine(ref handler); } int num8 = num2 + array2.Length; for (int num9 = num8; num9 <= num4 && num9 < array.Length; num9++) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder9 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(array[num9].TrimEnd('\r')); stringBuilder9.AppendLine(ref handler); } return stringBuilder.ToString().TrimEnd(); } private static int CountOccurrences(string text, string search) { if (string.IsNullOrEmpty(search)) { return 0; } int num = 0; int startIndex = 0; while ((startIndex = text.IndexOf(search, startIndex, StringComparison.Ordinal)) != -1) { num++; startIndex += search.Length; } return num; } }