Initial commit to new repository

This commit is contained in:
2026-04-03 18:22:19 +09:00
commit 4458bb0f52
7672 changed files with 452440 additions and 0 deletions
@@ -0,0 +1,171 @@
using System.IO;
using System.Text;
using System.Text.Json;
namespace AxCopilot.Services.Agent;
/// <summary>
/// 작업 폴더의 디렉토리 트리 구조를 생성하는 도구.
/// LLM이 프로젝트 전체 구조를 파악하고 적절한 파일을 찾을 수 있도록 돕습니다.
/// </summary>
public class FolderMapTool : IAgentTool
{
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()
{
Properties = new()
{
["path"] = new() { Type = "string", Description = "Subdirectory to map. Optional, defaults to work folder root." },
["depth"] = new() { Type = "integer", Description = "Maximum depth to traverse (1-10). Default: 3." },
["include_files"] = new() { Type = "boolean", Description = "Whether to include files. Default: true." },
["pattern"] = new() { Type = "string", Description = "File extension filter (e.g. '.cs', '.py'). Optional, shows all files if omitted." },
},
Required = []
};
// 무시할 디렉토리 (빌드 산출물, 패키지 캐시 등)
private static readonly HashSet<string> IgnoredDirs = new(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 Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
{
var subPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "";
var depth = 3;
if (args.TryGetProperty("depth", out var d))
{
if (d.ValueKind == JsonValueKind.Number) depth = d.GetInt32();
else if (d.ValueKind == JsonValueKind.String && int.TryParse(d.GetString(), out var dv)) depth = dv;
}
var depthStr = depth.ToString();
var includeFiles = true;
if (args.TryGetProperty("include_files", out var inc))
{
if (inc.ValueKind == JsonValueKind.True || inc.ValueKind == JsonValueKind.False)
includeFiles = inc.GetBoolean();
else
includeFiles = !string.Equals(inc.GetString(), "false", StringComparison.OrdinalIgnoreCase);
}
var extFilter = args.TryGetProperty("pattern", out var pat) ? pat.GetString() ?? "" : "";
if (!int.TryParse(depthStr, out var maxDepth) || maxDepth < 1)
maxDepth = 3;
maxDepth = Math.Min(maxDepth, 10);
var baseDir = string.IsNullOrEmpty(subPath)
? context.WorkFolder
: FileReadTool.ResolvePath(subPath, context.WorkFolder);
if (string.IsNullOrEmpty(baseDir) || !Directory.Exists(baseDir))
return Task.FromResult(ToolResult.Fail($"디렉토리가 존재하지 않습니다: {baseDir}"));
if (!context.IsPathAllowed(baseDir))
return Task.FromResult(ToolResult.Fail($"경로 접근 차단: {baseDir}"));
try
{
var sb = new StringBuilder();
var dirName = Path.GetFileName(baseDir);
if (string.IsNullOrEmpty(dirName)) dirName = baseDir;
sb.AppendLine($"{dirName}/");
int entryCount = 0;
BuildTree(sb, baseDir, "", 0, maxDepth, includeFiles, extFilter, context, ref entryCount);
if (entryCount >= MaxEntries)
sb.AppendLine($"\n... ({MaxEntries}개 항목 제한 도달, depth 또는 pattern을 조정하세요)");
var summary = $"폴더 맵 생성 완료 ({entryCount}개 항목, 깊이 {maxDepth})";
return Task.FromResult(ToolResult.Ok($"{summary}\n\n{sb}"));
}
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 >= MaxEntries) return;
// 하위 디렉토리
List<DirectoryInfo> subDirs;
try
{
subDirs = new DirectoryInfo(dir).GetDirectories()
.Where(d => !d.Attributes.HasFlag(FileAttributes.Hidden)
&& !IgnoredDirs.Contains(d.Name))
.OrderBy(d => d.Name)
.ToList();
}
catch { return; } // 접근 불가 디렉토리 무시
// 하위 파일
List<FileInfo> files = [];
if (includeFiles)
{
try
{
files = new DirectoryInfo(dir).GetFiles()
.Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden)
&& (string.IsNullOrEmpty(extFilter)
|| f.Extension.Equals(extFilter, StringComparison.OrdinalIgnoreCase)))
.OrderBy(f => f.Name)
.ToList();
}
catch { /* ignore */ }
}
var totalItems = subDirs.Count + files.Count;
var index = 0;
// 디렉토리 출력
foreach (var sub in subDirs)
{
if (entryCount >= MaxEntries) break;
index++;
var isLast = index == totalItems;
var connector = isLast ? "└── " : "├── ";
var childPrefix = isLast ? " " : "│ ";
sb.AppendLine($"{prefix}{connector}{sub.Name}/");
entryCount++;
if (context.IsPathAllowed(sub.FullName))
BuildTree(sb, sub.FullName, prefix + childPrefix, currentDepth + 1, maxDepth,
includeFiles, extFilter, context, ref entryCount);
}
// 파일 출력
foreach (var file in files)
{
if (entryCount >= MaxEntries) break;
index++;
var isLast = index == totalItems;
var connector = isLast ? "└── " : "├── ";
var sizeStr = FormatSize(file.Length);
sb.AppendLine($"{prefix}{connector}{file.Name} ({sizeStr})");
entryCount++;
}
}
private static string FormatSize(long bytes) => bytes switch
{
< 1024 => $"{bytes} B",
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
_ => $"{bytes / (1024.0 * 1024.0):F1} MB",
};
}