Files
meeting-assistant/MeetingAssistant/AgentFileToolContent.cs
codex fd8c33f1ea
PR and Push Build/Test / build-and-test (push) Failing after 12m50s
Fix settings log tail reads
2026-05-30 14:25:35 +02:00

280 lines
8.4 KiB
C#

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<string> 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<AgentFileSearchSource> sources,
int maxMatches = 100,
RegexOptions regexOptions = RegexOptions.IgnoreCase)
{
if (string.IsNullOrWhiteSpace(keywords))
{
return "";
}
try
{
var regex = new Regex(keywords, regexOptions);
var matches = new List<string>();
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<string> 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
}