using System.Text.RegularExpressions; namespace MeetingAssistant; internal static class AgentFileToolContent { public static string ReadLines(string content, int? from = null, int? to = null, int? tail = null) { if (!from.HasValue && !to.HasValue) { return tail.HasValue ? TailLines(content, Math.Clamp(tail.Value, 1, 2000)) : content; } var lines = SplitLines(content); if (lines.Count == 0) { return ""; } var start = Math.Clamp((from ?? 1) - 1, 0, lines.Count - 1); var end = Math.Clamp((to ?? lines.Count) - 1, 0, lines.Count - 1); if (end < start) { return ""; } return string.Join('\n', lines.GetRange(start, end - start + 1)); } public static string TailLines(string content, int count) { var lines = SplitLines(content); if (lines.Count > 0 && lines[^1].Length == 0) { lines.RemoveAt(lines.Count - 1); } if (lines.Count <= count) { return string.Join('\n', lines); } return string.Join('\n', lines.GetRange(lines.Count - count, count)); } public static string ApplyLineEdit( string existingContent, string replacementContent, AgentFileEditMode editMode) { if (editMode.Kind == AgentFileEditKind.Overwrite) { return replacementContent; } if (editMode.Kind == AgentFileEditKind.Append) { return AppendContent(existingContent, replacementContent); } var lines = SplitLines(existingContent); var replacementLines = SplitLines(replacementContent); if (editMode.Kind == AgentFileEditKind.Insert) { var insertIndex = Math.Clamp(editMode.Insert!.Value - 1, 0, lines.Count); lines.InsertRange(insertIndex, replacementLines); return string.Join('\n', lines); } var startIndex = Math.Clamp(editMode.From!.Value - 1, 0, lines.Count); var endIndexExclusive = Math.Clamp(editMode.To!.Value, 0, lines.Count); if (endIndexExclusive < startIndex) { endIndexExclusive = startIndex; } lines.RemoveRange(startIndex, endIndexExclusive - startIndex); lines.InsertRange(startIndex, replacementLines); return string.Join('\n', lines); } public static async Task WriteFileContentAsync( string filePath, string content, AgentFileEditMode editMode) { if (editMode.Kind == AgentFileEditKind.Overwrite) { await File.WriteAllTextAsync(filePath, content); return; } if (editMode.Kind == AgentFileEditKind.Append) { var existing = File.Exists(filePath) ? await File.ReadAllTextAsync(filePath) : ""; await File.WriteAllTextAsync(filePath, AppendContent(existing, content)); return; } var existingContent = File.Exists(filePath) ? await File.ReadAllTextAsync(filePath) : ""; await File.WriteAllTextAsync(filePath, ApplyLineEdit(existingContent, content, editMode)); } public static string AppendContent(string existingContent, string content) { if (string.IsNullOrEmpty(existingContent)) { return content; } if (string.IsNullOrEmpty(content)) { return existingContent; } var separator = existingContent.EndsWith('\n') || existingContent.EndsWith('\r') ? "" : "\n"; return existingContent + separator + content; } public static List SplitLines(string content) { if (content.Length == 0) { return []; } return content .Replace("\r\n", "\n", StringComparison.Ordinal) .Replace('\r', '\n') .Split('\n') .ToList(); } public static bool IsWithinDirectory(string directory, string path) { var normalizedDirectory = Path.GetFullPath(directory) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; var normalizedPath = Path.GetFullPath(path); return normalizedPath.StartsWith(normalizedDirectory, StringComparison.OrdinalIgnoreCase); } public static bool MatchesGlob(string path, string? pattern) { if (string.IsNullOrWhiteSpace(pattern)) { return true; } var normalizedPath = ToToolPath(path); var normalizedPattern = ToToolPath(pattern.Trim()); var placeholder = "\u0000"; var regexPattern = Regex.Escape(normalizedPattern) .Replace("\\*\\*", placeholder, StringComparison.Ordinal) .Replace("\\*", "[^/]*", StringComparison.Ordinal) .Replace("\\?", "[^/]", StringComparison.Ordinal) .Replace(placeholder, ".*", StringComparison.Ordinal); return Regex.IsMatch(normalizedPath, $"^{regexPattern}$", RegexOptions.IgnoreCase); } public static string ToToolPath(string path) { return path.Replace(Path.DirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal) .Replace(Path.AltDirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal); } public static string SearchFiles( string keywords, IEnumerable sources, int maxMatches = 100, RegexOptions regexOptions = RegexOptions.IgnoreCase) { if (string.IsNullOrWhiteSpace(keywords)) { return ""; } try { var regex = new Regex(keywords, regexOptions); var matches = new List(); var limit = Math.Clamp(maxMatches, 1, 1000); foreach (var source in sources) { var lines = ReadAllLinesShared(source.Path); for (var index = 0; index < lines.Length; index++) { if (regex.IsMatch(lines[index])) { matches.Add($"{ToToolPath(source.DisplayPath)}:{index + 1} {lines[index]}"); } if (matches.Count >= limit) { return string.Join('\n', matches); } } } return string.Join('\n', matches); } catch { return ""; } } public static async Task ReadAllTextSharedAsync(string path) { await using var stream = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, useAsync: true); using var reader = new StreamReader(stream); return await reader.ReadToEndAsync(); } private static string[] ReadAllLinesShared(string path) { using var stream = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using var reader = new StreamReader(stream); return SplitLines(reader.ReadToEnd()).ToArray(); } } internal sealed record AgentFileSearchSource(string Path, string DisplayPath); internal sealed record AgentFileEditMode( AgentFileEditKind Kind, int? From = null, int? To = null, int? Insert = null) { public static AgentFileEditMode? Create(int? from, int? to, int? insert, bool replaceFile) { if (replaceFile && (from.HasValue || to.HasValue || insert.HasValue)) { return null; } if (insert.HasValue) { return from.HasValue || to.HasValue ? null : new AgentFileEditMode(AgentFileEditKind.Insert, Insert: insert); } if (from.HasValue || to.HasValue) { return from.HasValue && to.HasValue && from.Value <= to.Value ? new AgentFileEditMode(AgentFileEditKind.Replace, From: from, To: to) : null; } return new AgentFileEditMode(replaceFile ? AgentFileEditKind.Overwrite : AgentFileEditKind.Append); } } internal enum AgentFileEditKind { Append, Overwrite, Replace, Insert }