using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class FolderMapTool : IAgentTool { private static readonly HashSet IgnoredDirs = new HashSet(StringComparer.OrdinalIgnoreCase) { "bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode", "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", "packages", ".nuget", "TestResults", "coverage", ".next", "target", ".gradle", ".cargo" }; private const int MaxEntries = 500; public string Name => "folder_map"; public string Description => "Generate a directory tree map of the work folder or a specified subfolder. Shows folders and files in a tree structure. Use this to understand the project layout before reading or editing files."; public ToolParameterSchema Parameters => new ToolParameterSchema { Properties = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "Subdirectory to map. Optional, defaults to work folder root." }, ["depth"] = new ToolProperty { Type = "integer", Description = "Maximum depth to traverse (1-10). Default: 3." }, ["include_files"] = new ToolProperty { Type = "boolean", Description = "Whether to include files. Default: true." }, ["pattern"] = new ToolProperty { Type = "string", Description = "File extension filter (e.g. '.cs', '.py'). Optional, shows all files if omitted." } }, Required = new List() }; public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { JsonElement value; string text = (args.TryGetProperty("path", out value) ? (value.GetString() ?? "") : ""); int num = 3; if (args.TryGetProperty("depth", out var value2)) { int result; if (value2.ValueKind == JsonValueKind.Number) { num = value2.GetInt32(); } else if (value2.ValueKind == JsonValueKind.String && int.TryParse(value2.GetString(), out result)) { num = result; } } string s = num.ToString(); bool includeFiles = true; if (args.TryGetProperty("include_files", out var value3)) { includeFiles = ((value3.ValueKind != JsonValueKind.True && value3.ValueKind != JsonValueKind.False) ? (!string.Equals(value3.GetString(), "false", StringComparison.OrdinalIgnoreCase)) : value3.GetBoolean()); } JsonElement value4; string extFilter = (args.TryGetProperty("pattern", out value4) ? (value4.GetString() ?? "") : ""); if (!int.TryParse(s, out var result2) || result2 < 1) { result2 = 3; } result2 = Math.Min(result2, 10); string text2 = (string.IsNullOrEmpty(text) ? context.WorkFolder : FileReadTool.ResolvePath(text, context.WorkFolder)); if (string.IsNullOrEmpty(text2) || !Directory.Exists(text2)) { return Task.FromResult(ToolResult.Fail("디렉토리가 존재하지 않습니다: " + text2)); } if (!context.IsPathAllowed(text2)) { return Task.FromResult(ToolResult.Fail("경로 접근 차단: " + text2)); } try { StringBuilder stringBuilder = new StringBuilder(); string value5 = Path.GetFileName(text2); if (string.IsNullOrEmpty(value5)) { value5 = text2; } StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendFormatted(value5); handler.AppendLiteral("/"); stringBuilder3.AppendLine(ref handler); int entryCount = 0; BuildTree(stringBuilder, text2, "", 0, result2, includeFiles, extFilter, context, ref entryCount); if (entryCount >= 500) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(42, 1, stringBuilder2); handler.AppendLiteral("\n... ("); handler.AppendFormatted(500); handler.AppendLiteral("개 항목 제한 도달, depth 또는 pattern을 조정하세요)"); stringBuilder4.AppendLine(ref handler); } string value6 = $"폴더 맵 생성 완료 ({entryCount}개 항목, 깊이 {result2})"; return Task.FromResult(ToolResult.Ok($"{value6}\n\n{stringBuilder}")); } catch (Exception ex) { return Task.FromResult(ToolResult.Fail("폴더 맵 생성 실패: " + ex.Message)); } } private static void BuildTree(StringBuilder sb, string dir, string prefix, int currentDepth, int maxDepth, bool includeFiles, string extFilter, AgentContext context, ref int entryCount) { if (currentDepth >= maxDepth || entryCount >= 500) { return; } List list; try { list = (from d in new DirectoryInfo(dir).GetDirectories() where !d.Attributes.HasFlag(FileAttributes.Hidden) && !IgnoredDirs.Contains(d.Name) orderby d.Name select d).ToList(); } catch { return; } List list2 = new List(); if (includeFiles) { try { list2 = (from f in new DirectoryInfo(dir).GetFiles() where !f.Attributes.HasFlag(FileAttributes.Hidden) && (string.IsNullOrEmpty(extFilter) || f.Extension.Equals(extFilter, StringComparison.OrdinalIgnoreCase)) orderby f.Name select f).ToList(); } catch { } } int num = list.Count + list2.Count; int num2 = 0; foreach (DirectoryInfo item in list) { if (entryCount >= 500) { break; } num2++; bool flag = num2 == num; string value = (flag ? "└── " : "├── "); string text = (flag ? " " : "│ "); StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(1, 3, stringBuilder); handler.AppendFormatted(prefix); handler.AppendFormatted(value); handler.AppendFormatted(item.Name); handler.AppendLiteral("/"); stringBuilder2.AppendLine(ref handler); entryCount++; if (context.IsPathAllowed(item.FullName)) { BuildTree(sb, item.FullName, prefix + text, currentDepth + 1, maxDepth, includeFiles, extFilter, context, ref entryCount); } } foreach (FileInfo item2 in list2) { if (entryCount >= 500) { break; } num2++; string value2 = ((num2 == num) ? "└── " : "├── "); string value3 = FormatSize(item2.Length); StringBuilder stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 4, stringBuilder); handler.AppendFormatted(prefix); handler.AppendFormatted(value2); handler.AppendFormatted(item2.Name); handler.AppendLiteral(" ("); handler.AppendFormatted(value3); handler.AppendLiteral(")"); stringBuilder3.AppendLine(ref handler); entryCount++; } } private static string FormatSize(long bytes) { if (1 == 0) { } string result = ((bytes < 1024) ? $"{bytes} B" : ((bytes >= 1048576) ? $"{(double)bytes / 1048576.0:F1} MB" : $"{(double)bytes / 1024.0:F1} KB")); if (1 == 0) { } return result; } }