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.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class FileWatchTool : IAgentTool { public string Name => "file_watch"; public string Description => "Detect recent file changes in a folder. Returns a list of created, modified, and deleted files since a given time. Useful for monitoring data folders, detecting log updates, or tracking file system changes."; public ToolParameterSchema Parameters { get { ToolParameterSchema obj = new ToolParameterSchema { Properties = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "Folder path to watch. Relative to work folder." }, ["pattern"] = new ToolProperty { Type = "string", Description = "File pattern filter (e.g. '*.csv', '*.log', '*.xlsx'). Default: '*' (all files)" }, ["since"] = new ToolProperty { Type = "string", Description = "Time threshold: ISO 8601 datetime (e.g. '2026-03-30T09:00:00') or relative duration ('1h', '6h', '24h', '7d', '30d'). Default: '24h'" }, ["recursive"] = new ToolProperty { Type = "boolean", Description = "Search subdirectories recursively. Default: true" }, ["include_size"] = new ToolProperty { Type = "boolean", Description = "Include file sizes in output. Default: true" }, ["top_n"] = new ToolProperty { Type = "integer", Description = "Limit results to most recent N files. Default: 50" } } }; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); CollectionsMarshal.AsSpan(list)[0] = "path"; obj.Required = list; return obj; } } public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { string text = args.GetProperty("path").GetString() ?? ""; JsonElement value; string text2 = (args.TryGetProperty("pattern", out value) ? (value.GetString() ?? "*") : "*"); JsonElement value2; string text3 = (args.TryGetProperty("since", out value2) ? (value2.GetString() ?? "24h") : "24h"); JsonElement value3; bool flag = !args.TryGetProperty("recursive", out value3) || value3.GetBoolean(); JsonElement value4; bool includeSize = !args.TryGetProperty("include_size", out value4) || value4.GetBoolean(); JsonElement value5; int value6; int count = ((args.TryGetProperty("top_n", out value5) && value5.TryGetInt32(out value6)) ? value6 : 50); string text4 = FileReadTool.ResolvePath(text, context.WorkFolder); if (!context.IsPathAllowed(text4)) { return Task.FromResult(ToolResult.Fail("경로 접근 차단: " + text4)); } if (!Directory.Exists(text4)) { return Task.FromResult(ToolResult.Fail("폴더 없음: " + text4)); } try { DateTime since = ParseSince(text3); SearchOption searchOption = (flag ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); List list = (from f in Directory.GetFiles(text4, text2, searchOption) select new FileInfo(f) into fi where fi.LastWriteTime >= since || fi.CreationTime >= since orderby fi.LastWriteTime descending select fi).Take(count).ToList(); if (list.Count == 0) { return Task.FromResult(ToolResult.Ok($"\ud83d\udcc2 {text3} 이내 변경된 파일이 없습니다. (경로: {text}, 패턴: {text2})")); } StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(23, 2, stringBuilder2); handler.AppendLiteral("\ud83d\udcc2 파일 변경 감지: "); handler.AppendFormatted(list.Count); handler.AppendLiteral("개 파일 ("); handler.AppendFormatted(text3); handler.AppendLiteral(" 이내)"); stringBuilder3.AppendLine(ref handler); stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(14, 2, stringBuilder2); handler.AppendLiteral(" 경로: "); handler.AppendFormatted(text); handler.AppendLiteral(" | 패턴: "); handler.AppendFormatted(text2); stringBuilder4.AppendLine(ref handler); stringBuilder.AppendLine(); List list2 = list.Where((FileInfo f) => f.CreationTime >= since && f.CreationTime == f.LastWriteTime).ToList(); List list3 = list.Where((FileInfo f) => f.LastWriteTime >= since && f.CreationTime < since).ToList(); List list4 = list.Where((FileInfo f) => f.CreationTime >= since && f.CreationTime != f.LastWriteTime).ToList(); if (list2.Count > 0) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder5 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder2); handler.AppendLiteral("\ud83c\udd95 신규 생성 ("); handler.AppendFormatted(list2.Count); handler.AppendLiteral("개):"); stringBuilder5.AppendLine(ref handler); foreach (FileInfo item in list2) { AppendFileInfo(stringBuilder, item, text4, includeSize); } stringBuilder.AppendLine(); } if (list3.Count > 0) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder6 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2); handler.AppendLiteral("✏\ufe0f 수정됨 ("); handler.AppendFormatted(list3.Count); handler.AppendLiteral("개):"); stringBuilder6.AppendLine(ref handler); foreach (FileInfo item2 in list3) { AppendFileInfo(stringBuilder, item2, text4, includeSize); } stringBuilder.AppendLine(); } if (list4.Count > 0) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder7 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(16, 1, stringBuilder2); handler.AppendLiteral("\ud83d\udcdd 생성 후 수정됨 ("); handler.AppendFormatted(list4.Count); handler.AppendLiteral("개):"); stringBuilder7.AppendLine(ref handler); foreach (FileInfo item3 in list4) { AppendFileInfo(stringBuilder, item3, text4, includeSize); } stringBuilder.AppendLine(); } long bytes = list.Sum((FileInfo f) => f.Length); stringBuilder2 = stringBuilder; StringBuilder stringBuilder8 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(15, 2, stringBuilder2); handler.AppendLiteral("── 요약: 총 "); handler.AppendFormatted(list.Count); handler.AppendLiteral("개 파일, "); handler.AppendFormatted(FormatSize(bytes)); stringBuilder8.AppendLine(ref handler); IEnumerable> source = (from f in list group f by f.Extension.ToLowerInvariant() into g orderby g.Count() descending select g).Take(10); stringBuilder.Append(" 유형: "); stringBuilder.AppendLine(string.Join(", ", source.Select((IGrouping g) => $"{g.Key}({g.Count()})"))); return Task.FromResult(ToolResult.Ok(stringBuilder.ToString())); } catch (Exception ex) { return Task.FromResult(ToolResult.Fail("파일 감시 실패: " + ex.Message)); } } private static DateTime ParseSince(string since) { if (DateTime.TryParse(since, out var result)) { return result; } Match match = Regex.Match(since, "^(\\d+)(h|d|m)$"); if (match.Success) { int num = int.Parse(match.Groups[1].Value); string value = match.Groups[2].Value; if (1 == 0) { } DateTime result2 = value switch { "h" => DateTime.Now.AddHours(-num), "d" => DateTime.Now.AddDays(-num), "m" => DateTime.Now.AddMinutes(-num), _ => DateTime.Now.AddHours(-24.0), }; if (1 == 0) { } return result2; } return DateTime.Now.AddHours(-24.0); } private static void AppendFileInfo(StringBuilder sb, FileInfo f, string basePath, bool includeSize) { string relativePath = Path.GetRelativePath(basePath, f.FullName); string value = f.LastWriteTime.ToString("MM-dd HH:mm"); if (includeSize) { StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(8, 3, stringBuilder); handler.AppendLiteral(" "); handler.AppendFormatted(relativePath); handler.AppendLiteral(" ("); handler.AppendFormatted(FormatSize(f.Length)); handler.AppendLiteral(", "); handler.AppendFormatted(value); handler.AppendLiteral(")"); stringBuilder2.AppendLine(ref handler); } else { StringBuilder stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(6, 2, stringBuilder); handler.AppendLiteral(" "); handler.AppendFormatted(relativePath); handler.AppendLiteral(" ("); handler.AppendFormatted(value); handler.AppendLiteral(")"); stringBuilder3.AppendLine(ref handler); } } private static string FormatSize(long bytes) { if (bytes >= 1024) { if (bytes >= 1048576) { if (bytes >= 1073741824) { return $"{(double)bytes / 1073741824.0:F2}GB"; } return $"{(double)bytes / 1048576.0:F1}MB"; } return $"{(double)bytes / 1024.0:F1}KB"; } return $"{bytes}B"; } }