From 0e209a67875bbf7bbc31e3a7fef4617c15fe204a Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sat, 10 Jan 2026 15:33:40 -0800 Subject: [PATCH 1/3] Implement tilde path expansion for file operations - Add PathHelpers.ExpandPath method to handle ~ and ~/path expansion - Update all file operation functions to use path expansion: - ViewFile - CreateFile - ReplaceOneInFile - ReplaceFileContent - ReplaceMultipleInFile - Insert - UndoEdit - ListFiles - Fixes issue where file functions couldn't handle tilde paths like ~/.bashrc - Uses Environment.GetFolderPath(SpecialFolder.UserProfile) for cross-platform compatibility --- TildeTest.cs | 28 +++++++++ src/common/Helpers/PathHelpers.cs | 32 ++++++++++ .../StrReplaceEditorHelperFunctions.cs | 25 ++++++++ tests/cycodt-yaml/tilde-path-expansion.yaml | 63 +++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 TildeTest.cs create mode 100644 tests/cycodt-yaml/tilde-path-expansion.yaml diff --git a/TildeTest.cs b/TildeTest.cs new file mode 100644 index 000000000..bb697ede5 --- /dev/null +++ b/TildeTest.cs @@ -0,0 +1,28 @@ +using System; + +namespace TildeTest +{ + class Program + { + static void Main() + { + Console.WriteLine("Testing PathHelpers.ExpandPath function:"); + + // Test cases + string[] testPaths = { + "~", + "~/test.txt", + "~/.bashrc", + "/absolute/path", + "relative/path", + "~/Documents/test file.txt" + }; + + foreach (var path in testPaths) + { + var expanded = PathHelpers.ExpandPath(path); + Console.WriteLine($"'{path}' -> '{expanded}'"); + } + } + } +} \ No newline at end of file diff --git a/src/common/Helpers/PathHelpers.cs b/src/common/Helpers/PathHelpers.cs index eca4681a6..b48226d5b 100644 --- a/src/common/Helpers/PathHelpers.cs +++ b/src/common/Helpers/PathHelpers.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.IO; + public class PathHelpers { public static string? Combine(string path1, string path2) @@ -47,4 +51,32 @@ public static string NormalizePath(string outputDirectory) ? normalized.Substring(cwd.Length + 1) : normalized; } + + /// + /// Expands tilde (~) paths to full paths using the user's home directory. + /// Handles both "~" (home directory) and "~/path" (home directory + path). + /// + /// The path that may contain a tilde + /// The expanded path with tilde replaced by the home directory + public static string ExpandPath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // Handle exact "~" case + if (path == "~") + { + return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } + + // Handle "~/..." case + if (path.StartsWith("~/")) + { + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return Path.Combine(homeDir, path.Substring(2)); + } + + // Return unchanged if no tilde expansion needed + return path; + } } diff --git a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs index a32b5602d..d769da072 100644 --- a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs +++ b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs @@ -12,6 +12,9 @@ public class StrReplaceEditorHelperFunctions [Description("Returns a list of non-hidden files and directories up to 2 levels deep.")] public string ListFiles([Description("Absolute or relative path to directory.")] string path) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + if (Directory.Exists(path)) { path = Path.GetFullPath(path); @@ -54,6 +57,9 @@ public string ViewFile( [Description("Maximum number of characters to display per line.")] int maxCharsPerLine = 500, [Description("Maximum total number of characters to display.")] int maxTotalChars = 100000) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + // Basic file validation var noFile = Directory.Exists(path) || !File.Exists(path); if (noFile) return $"Path {path} does not exist or is not a file."; @@ -296,6 +302,9 @@ public string CreateFile( [Description("Absolute or relative path to file.")] string path, [Description("Content to be written to the file.")] string fileText) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + if (File.Exists(path)) { return $"Path {path} already exists; cannot create file. Use ViewFile and then ReplaceOneInFile to edit the file."; @@ -312,6 +321,9 @@ public string ReplaceFileContent( [Description("New content to replace the entire file.")] string newContent, [Description("Current line count of the file (for verification).")] int oldContentLineCount) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + if (!File.Exists(path)) { return $"File {path} does not exist. Use CreateFile to create a new file."; @@ -392,6 +404,9 @@ public string ReplaceOneInFile( [Description("Existing text in the file that should be replaced. Must match exactly one occurrence.")] string oldStr, [Description("New string content that will replace the old string.")] string newStr) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + if (!File.Exists(path)) { return $"File {path} does not exist."; @@ -422,6 +437,9 @@ public string ReplaceMultipleInFile( [Description("Array of old strings to be replaced. Each must match exactly one occurrence.")] string[] oldStrings, [Description("Array of new strings to replace with. Must be same length as oldStrings.")] string[] newStrings) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + if (!File.Exists(path)) { return $"File {path} does not exist."; @@ -491,6 +509,9 @@ public string Insert( [Description("Line number (1-indexed) after which to insert the new string.")] int insertLine, [Description("The string to insert into the file.")] string newStr) { + // Expand tilde paths before any file operations + path = PathHelpers.ExpandPath(path); + if (!File.Exists(path)) { return $"File {path} does not exist."; @@ -518,6 +539,10 @@ public string Insert( public string UndoEdit( [Description("Absolute or relative path to file.")] string path) { + // Expand tilde paths before any file operations + var originalPath = path; + path = PathHelpers.ExpandPath(path); + if (!EditHistory.ContainsKey(path)) { return $"No previous edit found for {path}."; diff --git a/tests/cycodt-yaml/tilde-path-expansion.yaml b/tests/cycodt-yaml/tilde-path-expansion.yaml new file mode 100644 index 000000000..9fe48312a --- /dev/null +++ b/tests/cycodt-yaml/tilde-path-expansion.yaml @@ -0,0 +1,63 @@ +class: tilde-path-expansion-tests +tag: file-operations +tests: + +- name: "Manual tilde expansion test" + bash: | + # Create test file in home + echo 'Testing tilde expansion manually' > ~/.test_manual.txt + + # Build the project first + dotnet build src/cycod/cycod.csproj > /dev/null + + # Test the ViewFile function directly + OUTPUT=$(dotnet run --project src/cycod/cycod.csproj -- --wait "ViewFile ~/.test_manual.txt" 2>/dev/null) + + # Check if tilde expansion worked + if echo "$OUTPUT" | grep -q "Testing tilde expansion manually"; then + echo "SUCCESS: Tilde expansion works!" + echo "Output contains the expected text" + else + echo "FAILURE: Tilde expansion failed" + echo "Output was: $OUTPUT" + fi + + # Cleanup + rm -f ~/.test_manual.txt + expect-regex: + - "SUCCESS: Tilde expansion works!" + +- name: "Compare before/after fix - should work now" + bash: | + # Create test file + echo 'Before and after test' > ~/.test_before_after.txt + + # Build first + dotnet build src/cycod/cycod.csproj > /dev/null + + # Test with tilde - should work now + TILDE_RESULT=$(dotnet run --project src/cycod/cycod.csproj -- --wait "ViewFile ~/.test_before_after.txt" 2>&1) + + # Test with full path - should also work + FULL_RESULT=$(dotnet run --project src/cycod/cycod.csproj -- --wait "ViewFile $HOME/.test_before_after.txt" 2>&1) + + # Check if tilde path works (doesn't give "does not exist" error) + if echo "$TILDE_RESULT" | grep -q "does not exist"; then + echo "FAILURE: Tilde path still not working" + elif echo "$TILDE_RESULT" | grep -q "Before and after test"; then + echo "SUCCESS: Tilde path expansion is working" + else + echo "UNCLEAR: Unexpected tilde result: $TILDE_RESULT" + fi + + # Check equivalence + if [ "$TILDE_RESULT" = "$FULL_RESULT" ]; then + echo "SUCCESS: Tilde and full paths give identical results" + else + echo "WARNING: Results differ between tilde and full path" + fi + + # Cleanup + rm -f ~/.test_before_after.txt + expect-regex: + - "SUCCESS: Tilde path expansion is working" \ No newline at end of file From fd9febababf3154d8afec7c63017cb190b5309a1 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sat, 10 Jan 2026 15:34:17 -0800 Subject: [PATCH 2/3] Clean up test files --- TildeTest.cs | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 TildeTest.cs diff --git a/TildeTest.cs b/TildeTest.cs deleted file mode 100644 index bb697ede5..000000000 --- a/TildeTest.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace TildeTest -{ - class Program - { - static void Main() - { - Console.WriteLine("Testing PathHelpers.ExpandPath function:"); - - // Test cases - string[] testPaths = { - "~", - "~/test.txt", - "~/.bashrc", - "/absolute/path", - "relative/path", - "~/Documents/test file.txt" - }; - - foreach (var path in testPaths) - { - var expanded = PathHelpers.ExpandPath(path); - Console.WriteLine($"'{path}' -> '{expanded}'"); - } - } - } -} \ No newline at end of file From ccefe0e88e823e112190ebce67d882d34664f93b Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sat, 10 Jan 2026 20:40:20 -0800 Subject: [PATCH 3/3] Add comprehensive tilde path expansion support to FileHelpers and function calling tools --- src/common/Helpers/FileHelpers.cs | 15 ++++- .../StrReplaceEditorHelperFunctions.cs | 17 +---- tests/cycodt-yaml/tilde-path-expansion.yaml | 63 ------------------- 3 files changed, 15 insertions(+), 80 deletions(-) delete mode 100644 tests/cycodt-yaml/tilde-path-expansion.yaml diff --git a/src/common/Helpers/FileHelpers.cs b/src/common/Helpers/FileHelpers.cs index 404ebe9aa..bce34cf31 100644 --- a/src/common/Helpers/FileHelpers.cs +++ b/src/common/Helpers/FileHelpers.cs @@ -18,6 +18,7 @@ public static IEnumerable FindFiles(string path, string pattern) public static IEnumerable FindFiles(string fileNames) { + fileNames = PathHelpers.ExpandPath(fileNames); var currentDir = Directory.GetCurrentDirectory(); foreach (var item in fileNames.Split(new char[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { @@ -103,11 +104,14 @@ public static IEnumerable FindFilesInOsPath(string fileName) public static bool FileExists(string? fileName) { - return !string.IsNullOrEmpty(fileName) && (File.Exists(fileName) || fileName == "-"); + if (string.IsNullOrEmpty(fileName)) return false; + fileName = PathHelpers.ExpandPath(fileName); + return File.Exists(fileName) || fileName == "-"; } public static bool IsFileMatch(string fileName, List includeFileContainsPatternList, List excludeFileContainsPatternList) { + fileName = PathHelpers.ExpandPath(fileName); var checkContent = includeFileContainsPatternList.Any() || excludeFileContainsPatternList.Any(); if (!checkContent) return true; @@ -185,6 +189,7 @@ public static bool IsFileMatch(string fileName, List includeFileContainsP public static void ReadIgnoreFile(string ignoreFile, out List excludeGlobs, out List excludeFileNamePatternList) { + ignoreFile = PathHelpers.ExpandPath(ignoreFile); ConsoleHelpers.WriteDebugLine($"ReadIgnoreFile: ignoreFile: {ignoreFile}"); excludeGlobs = new List(); @@ -355,6 +360,7 @@ public static bool IsFileTimeMatch(string fileName, DateTime? accessedAfter, DateTime? accessedBefore, DateTime? anyTimeAfter, DateTime? anyTimeBefore) { + fileName = PathHelpers.ExpandPath(fileName); try { var fileInfo = new FileInfo(fileName); @@ -436,6 +442,7 @@ public static string MakeRelativePath(string fullPath) public static string ReadAllText(string fileName) { + fileName = PathHelpers.ExpandPath(fileName); var content = ConsoleHelpers.IsStandardInputReference(fileName) ? string.Join("\n", ConsoleHelpers.GetAllLinesFromStdin()) : File.ReadAllText(fileName, Encoding.UTF8); @@ -445,6 +452,7 @@ public static string ReadAllText(string fileName) public static string[] ReadAllLines(string fileName) { + fileName = PathHelpers.ExpandPath(fileName); var lines = ConsoleHelpers.IsStandardInputReference(fileName) ? ConsoleHelpers.GetAllLinesFromStdin().ToArray() : File.ReadAllLines(fileName, Encoding.UTF8); @@ -454,6 +462,7 @@ public static string[] ReadAllLines(string fileName) public static string WriteAllText(string fileName, string content, string? saveToFolderOnAccessDenied = null) { + fileName = PathHelpers.ExpandPath(fileName); try { DirectoryHelpers.EnsureDirectoryForFileExists(fileName); @@ -478,6 +487,7 @@ public static string WriteAllText(string fileName, string content, string? saveT public static void AppendAllText(string fileName, string trajectoryContent) { + fileName = PathHelpers.ExpandPath(fileName); DirectoryHelpers.EnsureDirectoryForFileExists(fileName); File.AppendAllText(fileName, trajectoryContent, Encoding.UTF8); } @@ -638,6 +648,9 @@ private static char[] GetInvalidFileNameCharsForWeb() { if (fileNames == null || fileNames.Length == 0) return null; + + // Expand tilde paths for all filenames + fileNames = fileNames.Select(PathHelpers.ExpandPath).ToArray(); var currentPath = Directory.GetCurrentDirectory(); diff --git a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs index d769da072..e5c851f74 100644 --- a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs +++ b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs @@ -12,9 +12,8 @@ public class StrReplaceEditorHelperFunctions [Description("Returns a list of non-hidden files and directories up to 2 levels deep.")] public string ListFiles([Description("Absolute or relative path to directory.")] string path) { - // Expand tilde paths before any file operations path = PathHelpers.ExpandPath(path); - + if (Directory.Exists(path)) { path = Path.GetFullPath(path); @@ -57,7 +56,6 @@ public string ViewFile( [Description("Maximum number of characters to display per line.")] int maxCharsPerLine = 500, [Description("Maximum total number of characters to display.")] int maxTotalChars = 100000) { - // Expand tilde paths before any file operations path = PathHelpers.ExpandPath(path); // Basic file validation @@ -302,8 +300,6 @@ public string CreateFile( [Description("Absolute or relative path to file.")] string path, [Description("Content to be written to the file.")] string fileText) { - // Expand tilde paths before any file operations - path = PathHelpers.ExpandPath(path); if (File.Exists(path)) { @@ -321,8 +317,6 @@ public string ReplaceFileContent( [Description("New content to replace the entire file.")] string newContent, [Description("Current line count of the file (for verification).")] int oldContentLineCount) { - // Expand tilde paths before any file operations - path = PathHelpers.ExpandPath(path); if (!File.Exists(path)) { @@ -404,8 +398,6 @@ public string ReplaceOneInFile( [Description("Existing text in the file that should be replaced. Must match exactly one occurrence.")] string oldStr, [Description("New string content that will replace the old string.")] string newStr) { - // Expand tilde paths before any file operations - path = PathHelpers.ExpandPath(path); if (!File.Exists(path)) { @@ -437,8 +429,6 @@ public string ReplaceMultipleInFile( [Description("Array of old strings to be replaced. Each must match exactly one occurrence.")] string[] oldStrings, [Description("Array of new strings to replace with. Must be same length as oldStrings.")] string[] newStrings) { - // Expand tilde paths before any file operations - path = PathHelpers.ExpandPath(path); if (!File.Exists(path)) { @@ -509,8 +499,6 @@ public string Insert( [Description("Line number (1-indexed) after which to insert the new string.")] int insertLine, [Description("The string to insert into the file.")] string newStr) { - // Expand tilde paths before any file operations - path = PathHelpers.ExpandPath(path); if (!File.Exists(path)) { @@ -539,9 +527,6 @@ public string Insert( public string UndoEdit( [Description("Absolute or relative path to file.")] string path) { - // Expand tilde paths before any file operations - var originalPath = path; - path = PathHelpers.ExpandPath(path); if (!EditHistory.ContainsKey(path)) { diff --git a/tests/cycodt-yaml/tilde-path-expansion.yaml b/tests/cycodt-yaml/tilde-path-expansion.yaml deleted file mode 100644 index 9fe48312a..000000000 --- a/tests/cycodt-yaml/tilde-path-expansion.yaml +++ /dev/null @@ -1,63 +0,0 @@ -class: tilde-path-expansion-tests -tag: file-operations -tests: - -- name: "Manual tilde expansion test" - bash: | - # Create test file in home - echo 'Testing tilde expansion manually' > ~/.test_manual.txt - - # Build the project first - dotnet build src/cycod/cycod.csproj > /dev/null - - # Test the ViewFile function directly - OUTPUT=$(dotnet run --project src/cycod/cycod.csproj -- --wait "ViewFile ~/.test_manual.txt" 2>/dev/null) - - # Check if tilde expansion worked - if echo "$OUTPUT" | grep -q "Testing tilde expansion manually"; then - echo "SUCCESS: Tilde expansion works!" - echo "Output contains the expected text" - else - echo "FAILURE: Tilde expansion failed" - echo "Output was: $OUTPUT" - fi - - # Cleanup - rm -f ~/.test_manual.txt - expect-regex: - - "SUCCESS: Tilde expansion works!" - -- name: "Compare before/after fix - should work now" - bash: | - # Create test file - echo 'Before and after test' > ~/.test_before_after.txt - - # Build first - dotnet build src/cycod/cycod.csproj > /dev/null - - # Test with tilde - should work now - TILDE_RESULT=$(dotnet run --project src/cycod/cycod.csproj -- --wait "ViewFile ~/.test_before_after.txt" 2>&1) - - # Test with full path - should also work - FULL_RESULT=$(dotnet run --project src/cycod/cycod.csproj -- --wait "ViewFile $HOME/.test_before_after.txt" 2>&1) - - # Check if tilde path works (doesn't give "does not exist" error) - if echo "$TILDE_RESULT" | grep -q "does not exist"; then - echo "FAILURE: Tilde path still not working" - elif echo "$TILDE_RESULT" | grep -q "Before and after test"; then - echo "SUCCESS: Tilde path expansion is working" - else - echo "UNCLEAR: Unexpected tilde result: $TILDE_RESULT" - fi - - # Check equivalence - if [ "$TILDE_RESULT" = "$FULL_RESULT" ]; then - echo "SUCCESS: Tilde and full paths give identical results" - else - echo "WARNING: Results differ between tilde and full path" - fi - - # Cleanup - rm -f ~/.test_before_after.txt - expect-regex: - - "SUCCESS: Tilde path expansion is working" \ No newline at end of file