221 lines
7.1 KiB
C#
221 lines
7.1 KiB
C#
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<string> IgnoredDirs = new HashSet<string>(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<string, ToolProperty>
|
|
{
|
|
["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<string>()
|
|
};
|
|
|
|
public Task<ToolResult> 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<DirectoryInfo> 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<FileInfo> list2 = new List<FileInfo>();
|
|
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;
|
|
}
|
|
}
|