1
0

feat: consolidate file write, move, grep, and delete operations into unified tools and update context compaction heuristics

This commit is contained in:
2026-03-06 01:14:56 +01:00
parent 7a6e9785d6
commit 003345edc0
6 changed files with 230 additions and 261 deletions

View File

@@ -39,27 +39,6 @@ internal static class DirTools
}
}
[Description("Delete a directory and all its contents permanently.")]
public static string DeleteDir(
[Description("Path to the directory to delete.")] string path,
[Description("If true, delete recursively. Defaults to true.")] bool recursive = true)
{
path = ResolvePath(path);
Log($"Deleting directory: {path}");
if (!Directory.Exists(path))
return $"ERROR: Directory not found: {path}";
try
{
Directory.Delete(path, recursive);
return $"OK: Directory deleted: '{path}'";
}
catch (Exception ex)
{
return $"ERROR deleting directory '{path}': {ex.Message}";
}
}
[Description("Create a new directory. Creates parent directories if they don't exist. Returns OK on success, or an error message if the directory already exists or creation fails.")]
public static string CreateDir(
[Description("Path to the directory to create.")] string path)

View File

@@ -90,44 +90,6 @@ internal static partial class EditTools
}
}
[Description("Insert lines after the specified line:hash anchor.")]
public static string InsertAfter(
[Description("Path to the file.")] string path,
[Description("line:hash anchor to insert after (e.g. '3:0e').")] string anchor,
[Description("Raw source code to insert. Do NOT include 'lineNumber:hash|' prefixes.")] string[] newLines)
{
newLines = SanitizeNewLines(newLines);
path = FileTools.ResolvePath(path);
Log($"INSERT_AFTER: {path}");
Log($" Anchor: {anchor}");
Log($" Inserting {newLines.Length} lines after line {anchor.Split(':')[0]}");
if (!File.Exists(path))
return $"ERROR: File not found: {path}";
try
{
string[] lines = File.ReadAllLines(path);
if (!HashlineValidator.TryResolve(anchor, lines, out int idx, out string error))
return $"ERROR: {error}";
var result = new List<string>(lines.Length + newLines.Length);
result.AddRange(lines[..(idx + 1)]);
result.AddRange(newLines);
result.AddRange(lines[(idx + 1)..]);
File.WriteAllLines(path, result);
return $"OK fp:{HashlineEncoder.FileFingerprint([.. result])}";
}
catch (Exception ex)
{
return $"ERROR modifying '{path}': {ex.Message}";
}
}
[Description("Delete a range of lines.")]
public static string DeleteRange(
@@ -162,56 +124,46 @@ internal static partial class EditTools
}
}
[Description("Create a new file (parents auto-created). Max initial lines: 200. Alternatively, append lines later.")]
public static string CreateFile(
[Description("Path to the new file to create.")] string path,
[Description("Optional initial raw source code. Do NOT include 'lineNumber:hash|' prefixes.")] string[]? initialLines = null)
[Description("Delete a file or directory. Use mode='file' to delete a file, mode='dir' to delete a directory.")]
public static string Delete(
[Description("Path to the file or directory to delete.")] string path,
[Description("Type of deletion: 'file' or 'dir'. Defaults to 'file'.")] string mode = "file")
{
path = FileTools.ResolvePath(path);
Log($"Creating file: {path}");
string targetType = mode.ToLower() == "dir" ? "directory" : "file";
Log($"Deleting {targetType}: {path}");
if (File.Exists(path))
return $"ERROR: File already exists: {path}";
try
if (mode.ToLower() == "dir")
{
if (initialLines is not null)
initialLines = SanitizeNewLines(initialLines);
string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
if (!Directory.Exists(path))
return $"ERROR: Directory not found: {path}";
if (initialLines is not null && initialLines.Length > 0)
File.WriteAllLines(path, initialLines);
else
File.WriteAllText(path, "");
return $"OK fp:{HashlineEncoder.FileFingerprint(initialLines ?? [])}";
try
{
Directory.Delete(path, true);
return $"OK: Directory deleted: '{path}'";
}
catch (Exception ex)
{
return $"ERROR deleting directory '{path}': {ex.Message}";
}
}
catch (Exception ex)
else
{
return $"ERROR creating '{path}': {ex.Message}";
}
}
if (!File.Exists(path))
return $"ERROR: File not found: {path}";
[Description("Delete a file permanently.")]
public static string DeleteFile(
[Description("Path to the file to delete.")] string path)
{
path = FileTools.ResolvePath(path);
Log($"Deleting file: {path}");
if (!File.Exists(path))
return $"ERROR: File not found: {path}";
try
{
File.Delete(path);
return $"OK (deleted)";
}
catch (Exception ex)
{
return $"ERROR deleting '{path}': {ex.Message}";
try
{
File.Delete(path);
return $"OK (deleted)";
}
catch (Exception ex)
{
return $"ERROR deleting '{path}': {ex.Message}";
}
}
}
@@ -250,15 +202,19 @@ internal static partial class EditTools
}
}
[Description("Append lines to EOF (auto-creating the file if missing).")]
public static string AppendToFile(
[Description("Path to the file to append to.")] string path,
[Description("Raw source code to append. Do NOT include 'lineNumber:hash|' prefixes.")] string[] lines)
[Description("Write to a file with different modes: create, append, or insert.")]
public static string WriteToFile(
[Description("Path to the file.")] string path,
[Description("Content to write.")] string[] content,
[Description("Write mode: 'create' (error if exists), 'append' (creates if missing), 'insert' (requires anchor)")] string mode = "create",
[Description("line:hash anchor to insert after (required for mode='insert', e.g. '3:0e').")] string? anchor = null)
{
lines = SanitizeNewLines(lines);
content = SanitizeNewLines(content);
path = FileTools.ResolvePath(path);
Log($"Appending to file: {path}");
Log($" Appending {lines.Length} lines");
Log($"WRITE_TO_FILE: {path}");
Log($" Mode: {mode}");
Log($" Writing {content.Length} lines");
try
{
@@ -266,26 +222,64 @@ internal static partial class EditTools
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
if (!File.Exists(path))
switch (mode.ToLower())
{
File.WriteAllText(path, "");
Log($" (created new file)");
}
case "create":
if (File.Exists(path))
return $"ERROR: File already exists: {path}";
using (var writer = new System.IO.StreamWriter(path, true))
{
foreach (var line in lines)
{
writer.WriteLine(line);
}
}
if (content.Length > 0)
File.WriteAllLines(path, content);
else
File.WriteAllText(path, "");
string[] allLines = File.ReadAllLines(path);
return $"OK fp:{HashlineEncoder.FileFingerprint([.. allLines])}";
return $"OK fp:{HashlineEncoder.FileFingerprint(content)}";
case "append":
if (!File.Exists(path))
{
File.WriteAllText(path, "");
Log($" (created new file)");
}
using (var writer = new System.IO.StreamWriter(path, true))
{
foreach (var line in content)
{
writer.WriteLine(line);
}
}
string[] appendedLines = File.ReadAllLines(path);
return $"OK fp:{HashlineEncoder.FileFingerprint([.. appendedLines])}";
case "insert":
if (!File.Exists(path))
return $"ERROR: File not found: {path}";
if (string.IsNullOrEmpty(anchor))
return "ERROR: mode='insert' requires an anchor parameter";
string[] lines = File.ReadAllLines(path);
if (!HashlineValidator.TryResolve(anchor, lines, out int idx, out string error))
return $"ERROR: {error}";
var result = new List<string>(lines.Length + content.Length);
result.AddRange(lines[..(idx + 1)]);
result.AddRange(content);
result.AddRange(lines[(idx + 1)..]);
File.WriteAllLines(path, result);
return $"OK fp:{HashlineEncoder.FileFingerprint([.. result])}";
default:
return $"ERROR: Unknown mode '{mode}'. Valid modes: create, append, insert";
}
}
catch (Exception ex)
{
return $"ERROR appending to '{path}': {ex.Message}";
return $"ERROR writing to '{path}': {ex.Message}";
}
}

View File

@@ -56,54 +56,6 @@ internal static class FileTools
}
}
[Description("Search a file for a regex pattern. Returns matches with line:hash| anchors.")]
public static string GrepFile(
[Description("Path to the file to search.")] string path,
[Description("Regex pattern.")] string pattern)
{
path = ResolvePath(path);
Log($"Searching file: {path}");
if (!File.Exists(path))
return $"ERROR: File not found: {path}";
Regex regex;
try
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
return $"ERROR: Invalid regex pattern '{pattern}': {ex.Message}";
}
try
{
string[] lines = File.ReadAllLines(path);
var sb = new System.Text.StringBuilder();
int matchCount = 0;
for (int i = 0; i < lines.Length; i++)
{
if (regex.IsMatch(lines[i]))
{
int lineNumber = i + 1;
string hash = HashlineEncoder.ComputeHash(lines[i].AsSpan(), lineNumber);
sb.Append(lineNumber).Append(':').Append(hash).Append('|').AppendLine(lines[i]);
matchCount++;
}
}
if (matchCount == 0)
return $"(no matches for '{pattern}' in {path})";
return sb.ToString();
}
catch (Exception ex)
{
return $"ERROR searching '{path}': {ex.Message}";
}
}
[Description("List files and subdirectories.")]
public static string ListDir(
@@ -174,75 +126,130 @@ internal static class FileTools
}
}
[Description("Recursive regex search across all files. Returns matches with file:line:hash| format.")]
public static string GrepRecursive(
[Description("Directory to search.")] string path,
[Description("Consolidated grep operation for single file or recursive directory search.")]
public static string Grep(
[Description("Directory to search (for recursive mode) or file path (for file mode).")] string path,
[Description("Regex pattern.")] string pattern,
[Description("Optional glob to filter files (e.g. '*.cs').")] string? filePattern = null)
[Description("Mode: 'file' for single file, 'recursive' for directory search.")] string mode = "recursive",
[Description("Optional glob to filter files in recursive mode (e.g. '*.cs').")] string? filePattern = null)
{
path = ResolvePath(path);
Log($"Recursive grep: {pattern} in {path}" + (filePattern != null ? $" (files: {filePattern})" : ""));
mode = mode.ToLowerInvariant();
if (!Directory.Exists(path))
return $"ERROR: Directory not found: {path}";
Regex regex;
try
if (mode == "file")
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
return $"ERROR: Invalid regex pattern '{pattern}': {ex.Message}";
}
Log($"Searching file: {path}");
try
{
string globPattern = filePattern?.Replace("**/", "") ?? "*";
var sb = new System.Text.StringBuilder();
int totalMatches = 0;
if (!File.Exists(path))
return $"ERROR: File not found: {path}";
foreach (var file in EnumerateFilesRecursive(path, globPattern))
Regex regex;
try
{
try
{
// Skip binary files: check first 512 bytes for null chars
using var probe = new StreamReader(file);
var buf = new char[512];
int read = probe.Read(buf, 0, buf.Length);
if (new ReadOnlySpan<char>(buf, 0, read).Contains('\0'))
continue;
}
catch { continue; }
try
{
string[] lines = File.ReadAllLines(file);
for (int i = 0; i < lines.Length; i++)
{
if (regex.IsMatch(lines[i]))
{
int lineNumber = i + 1;
string hash = HashlineEncoder.ComputeHash(lines[i].AsSpan(), lineNumber);
sb.Append(file).Append(':').Append(lineNumber).Append(':').Append(hash).Append('|').AppendLine(lines[i]);
totalMatches++;
}
}
}
catch
{
// Skip files that can't be read
}
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
return $"ERROR: Invalid regex pattern '{pattern}': {ex.Message}";
}
if (totalMatches == 0)
return $"(no matches for '{pattern}' in {path})";
try
{
string[] lines = File.ReadAllLines(path);
var sb = new System.Text.StringBuilder();
int matchCount = 0;
return $"Found {totalMatches} match(es):\n" + sb.ToString();
for (int i = 0; i < lines.Length; i++)
{
if (regex.IsMatch(lines[i]))
{
int lineNumber = i + 1;
string hash = HashlineEncoder.ComputeHash(lines[i].AsSpan(), lineNumber);
sb.Append(lineNumber).Append(':').Append(hash).Append('|').AppendLine(lines[i]);
matchCount++;
}
}
if (matchCount == 0)
return $"(no matches for '{pattern}' in {path})";
return sb.ToString();
}
catch (Exception ex)
{
return $"ERROR searching '{path}': {ex.Message}";
}
}
catch (Exception ex)
else if (mode == "recursive")
{
return $"ERROR in recursive grep: {ex.Message}";
Log($"Recursive grep: {pattern} in {path}" + (filePattern != null ? $" (files: {filePattern})" : ""));
if (!Directory.Exists(path))
return $"ERROR: Directory not found: {path}";
Regex regex;
try
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
return $"ERROR: Invalid regex pattern '{pattern}': {ex.Message}";
}
try
{
string globPattern = filePattern?.Replace("**/", "") ?? "*";
var sb = new System.Text.StringBuilder();
int totalMatches = 0;
foreach (var file in EnumerateFilesRecursive(path, globPattern))
{
try
{
// Skip binary files: check first 512 bytes for null chars
using var probe = new StreamReader(file);
var buf = new char[512];
int read = probe.Read(buf, 0, buf.Length);
if (new ReadOnlySpan<char>(buf, 0, read).Contains('\0'))
continue;
}
catch { continue; }
try
{
string[] lines = File.ReadAllLines(file);
for (int i = 0; i < lines.Length; i++)
{
if (regex.IsMatch(lines[i]))
{
int lineNumber = i + 1;
string hash = HashlineEncoder.ComputeHash(lines[i].AsSpan(), lineNumber);
sb.Append(file).Append(':').Append(lineNumber).Append(':').Append(hash).Append('|').AppendLine(lines[i]);
totalMatches++;
}
}
}
catch
{
// Skip files that can't be read
}
}
if (totalMatches == 0)
return $"(no matches for '{pattern}' in {path})";
return $"Found {totalMatches} match(es):\n" + sb.ToString();
}
catch (Exception ex)
{
return $"ERROR in recursive grep: {ex.Message}";
}
}
else
{
return $"ERROR: Invalid mode '{mode}'. Use 'file' or 'recursive'.";
}
}