Public Access
Generalize settings and logs assistant
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
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 = File.ReadAllLines(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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user