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.Json.Serialization; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class TaskTrackerTool : IAgentTool { private class TaskItem { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("title")] public string Title { get; set; } = ""; [JsonPropertyName("priority")] public string Priority { get; set; } = "medium"; [JsonPropertyName("created")] public string Created { get; set; } = ""; [JsonPropertyName("done")] public bool Done { get; set; } } private const string TaskFileName = ".ax/tasks.json"; public string Name => "task_tracker"; public string Description => "Track tasks/TODOs in the working folder. Actions: 'scan' — scan source files for TODO/FIXME/HACK/BUG comments; 'add' — add a task to .ax/tasks.json; 'list' — list tasks from .ax/tasks.json; 'done' — mark a task as completed."; public ToolParameterSchema Parameters { get { ToolParameterSchema toolParameterSchema = new ToolParameterSchema(); Dictionary dictionary = new Dictionary(); ToolProperty obj = new ToolProperty { Type = "string", Description = "Action: scan, add, list, done" }; int num = 4; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "scan"; span[1] = "add"; span[2] = "list"; span[3] = "done"; obj.Enum = list; dictionary["action"] = obj; dictionary["title"] = new ToolProperty { Type = "string", Description = "Task title (for 'add')" }; dictionary["priority"] = new ToolProperty { Type = "string", Description = "Priority: high, medium, low (for 'add', default: medium)" }; dictionary["id"] = new ToolProperty { Type = "integer", Description = "Task ID (for 'done')" }; dictionary["extensions"] = new ToolProperty { Type = "string", Description = "File extensions to scan, comma-separated (default: .cs,.py,.js,.ts,.java,.cpp,.c)" }; toolParameterSchema.Properties = dictionary; num = 1; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); CollectionsMarshal.AsSpan(list2)[0] = "action"; toolParameterSchema.Required = list2; return toolParameterSchema; } } public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken)) { string text = args.GetProperty("action").GetString() ?? ""; try { if (1 == 0) { } Task result = text switch { "scan" => Task.FromResult(ScanTodos(args, context)), "add" => Task.FromResult(AddTask(args, context)), "list" => Task.FromResult(ListTasks(context)), "done" => Task.FromResult(MarkDone(args, context)), _ => Task.FromResult(ToolResult.Fail("Unknown action: " + text)), }; if (1 == 0) { } return result; } catch (Exception ex) { return Task.FromResult(ToolResult.Fail("태스크 추적 오류: " + ex.Message)); } } private static ToolResult ScanTodos(JsonElement args, AgentContext context) { JsonElement value; string text = (args.TryGetProperty("extensions", out value) ? (value.GetString() ?? ".cs,.py,.js,.ts,.java,.cpp,.c") : ".cs,.py,.js,.ts,.java,.cpp,.c"); HashSet hashSet = new HashSet(from s in text.Split(',') select s.Trim().StartsWith('.') ? s.Trim() : ("." + s.Trim()), StringComparer.OrdinalIgnoreCase); string[] array = new string[5] { "TODO", "FIXME", "HACK", "BUG", "XXX" }; List<(string, int, string, string)> list = new List<(string, int, string, string)>(); string workFolder = context.WorkFolder; if (!Directory.Exists(workFolder)) { return ToolResult.Fail("작업 폴더 없음: " + workFolder); } foreach (string item5 in Directory.EnumerateFiles(workFolder, "*", SearchOption.AllDirectories)) { if (!hashSet.Contains(Path.GetExtension(item5)) || item5.Contains("bin") || item5.Contains("obj") || item5.Contains("node_modules")) { continue; } int num = 0; foreach (string item6 in File.ReadLines(item5)) { num++; string[] array2 = array; foreach (string text2 in array2) { int num3 = item6.IndexOf(text2, StringComparison.OrdinalIgnoreCase); if (num3 >= 0) { string text3 = item6; int num4 = num3 + text2.Length; string text4 = text3.Substring(num4, text3.Length - num4).TrimStart(':', ' '); if (text4.Length > 100) { text4 = text4.Substring(0, 100) + "..."; } string relativePath = Path.GetRelativePath(workFolder, item5); list.Add((relativePath, num, text2, text4)); break; } } if (list.Count >= 200) { break; } } if (list.Count < 200) { continue; } break; } if (list.Count == 0) { return ToolResult.Ok("TODO/FIXME 코멘트가 발견되지 않았습니다."); } StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder2); handler.AppendLiteral("발견된 TODO 코멘트: "); handler.AppendFormatted(list.Count); handler.AppendLiteral("개"); stringBuilder3.AppendLine(ref handler); foreach (var item7 in list) { string item = item7.Item1; int item2 = item7.Item2; string item3 = item7.Item3; string item4 = item7.Item4; stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 4, stringBuilder2); handler.AppendLiteral(" ["); handler.AppendFormatted(item3); handler.AppendLiteral("] "); handler.AppendFormatted(item); handler.AppendLiteral(":"); handler.AppendFormatted(item2); handler.AppendLiteral(" — "); handler.AppendFormatted(item4); stringBuilder4.AppendLine(ref handler); } return ToolResult.Ok(stringBuilder.ToString()); } private static ToolResult AddTask(JsonElement args, AgentContext context) { JsonElement value; string text = (args.TryGetProperty("title", out value) ? (value.GetString() ?? "") : ""); if (string.IsNullOrEmpty(text)) { return ToolResult.Fail("'title'이 필요합니다."); } JsonElement value2; string text2 = (args.TryGetProperty("priority", out value2) ? (value2.GetString() ?? "medium") : "medium"); List list = LoadTasks(context); int num = ((list.Count > 0) ? list.Max((TaskItem t2) => t2.Id) : 0); list.Add(new TaskItem { Id = num + 1, Title = text, Priority = text2, Created = DateTime.Now.ToString("yyyy-MM-dd HH:mm"), Done = false }); SaveTasks(context, list); return ToolResult.Ok($"태스크 추가: #{num + 1} — {text} (우선순위: {text2})"); } private static ToolResult ListTasks(AgentContext context) { List list = LoadTasks(context); if (list.Count == 0) { return ToolResult.Ok("등록된 태스크가 없습니다."); } StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2); handler.AppendLiteral("태스크 목록 ("); handler.AppendFormatted(list.Count); handler.AppendLiteral("개):"); stringBuilder3.AppendLine(ref handler); foreach (TaskItem item in from t in list orderby t.Done, (!(t.Priority == "high")) ? ((t.Priority == "medium") ? 1 : 2) : 0 descending select t) { string value = (item.Done ? "✓" : "○"); stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(11, 5, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(value); handler.AppendLiteral(" #"); handler.AppendFormatted(item.Id); handler.AppendLiteral(" ["); handler.AppendFormatted(item.Priority); handler.AppendLiteral("] "); handler.AppendFormatted(item.Title); handler.AppendLiteral(" ("); handler.AppendFormatted(item.Created); handler.AppendLiteral(")"); stringBuilder4.AppendLine(ref handler); } return ToolResult.Ok(stringBuilder.ToString()); } private static ToolResult MarkDone(JsonElement args, AgentContext context) { if (!args.TryGetProperty("id", out var value)) { return ToolResult.Fail("'id'가 필요합니다."); } int id = value.GetInt32(); List list = LoadTasks(context); TaskItem taskItem = list.FirstOrDefault((TaskItem t) => t.Id == id); if (taskItem == null) { return ToolResult.Fail($"태스크 #{id}를 찾을 수 없습니다."); } taskItem.Done = true; SaveTasks(context, list); return ToolResult.Ok($"태스크 #{id} 완료: {taskItem.Title}"); } private static List LoadTasks(AgentContext context) { string path = Path.Combine(context.WorkFolder, ".ax/tasks.json"); if (!File.Exists(path)) { return new List(); } string json = File.ReadAllText(path); return JsonSerializer.Deserialize>(json) ?? new List(); } private static void SaveTasks(AgentContext context, List tasks) { string path = Path.Combine(context.WorkFolder, ".ax/tasks.json"); string directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } string contents = JsonSerializer.Serialize(tasks, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(path, contents); } }