on main help sign in
fix(tools): fix handling of abs paths on windows
commit019b7a201164a53f22c8fc1240576a08515cc11d authorAndrey Nering <andreynering@users.noreply.github.com> dateMon 27 Oct 2025, 18:11 (1 year ago) parentfd5f830
7 files changed+39−24
internal/agent/tools/download.go+2−7 internal/agent/tools/edit.go+2−3 internal/agent/tools/ls.go+2−3 internal/agent/tools/multiedit.go+2−3 internal/agent/tools/view.go+2−4 internal/agent/tools/write.go+2−4 internal/filepathext/filepath.go+27
internal/agent/tools/download.go+2−7
@@ -12,6 +12,7 @@ import (
1212 "time"
1313
1414 "charm.land/fantasy"
15+ "github.com/charmbracelet/crush/internal/filepathext"
1516 "github.com/charmbracelet/crush/internal/permission"
1617)
1718
@@ -59,13 +60,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
5960 return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
6061 }
6162
62− // Convert relative path to absolute path
63− var filePath string
64− if filepath.IsAbs(params.FilePath) {
65− filePath = params.FilePath
66− } else {
67− filePath = filepath.Join(workingDir, params.FilePath)
68− }
63+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
6964
7065 sessionID := GetSessionFromContext(ctx)
7166 if sessionID == "" {
internal/agent/tools/edit.go+2−3
@@ -13,6 +13,7 @@ import (
1313 "charm.land/fantasy"
1414 "github.com/charmbracelet/crush/internal/csync"
1515 "github.com/charmbracelet/crush/internal/diff"
16+ "github.com/charmbracelet/crush/internal/filepathext"
1617 "github.com/charmbracelet/crush/internal/fsext"
1718 "github.com/charmbracelet/crush/internal/history"
1819
@@ -61,9 +62,7 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
6162 return fantasy.NewTextErrorResponse("file_path is required"), nil
6263 }
6364
64− if !filepath.IsAbs(params.FilePath) {
65− params.FilePath = filepath.Join(workingDir, params.FilePath)
66− }
65+ params.FilePath = filepathext.SmartJoin(workingDir, params.FilePath)
6766
6867 var response fantasy.ToolResponse
6968 var err error
internal/agent/tools/ls.go+2−3
@@ -11,6 +11,7 @@ import (
1111
1212 "charm.land/fantasy"
1313 "github.com/charmbracelet/crush/internal/config"
14+ "github.com/charmbracelet/crush/internal/filepathext"
1415 "github.com/charmbracelet/crush/internal/fsext"
1516 "github.com/charmbracelet/crush/internal/permission"
1617)
@@ -57,9 +58,7 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
5758 return fantasy.NewTextErrorResponse(fmt.Sprintf("error expanding path: %v", err)), nil
5859 }
5960
60− if !filepath.IsAbs(searchPath) {
61− searchPath = filepath.Join(workingDir, searchPath)
62− }
61+ searchPath = filepathext.SmartJoin(workingDir, searchPath)
6362
6463 // Check if directory is outside working directory and request permission if needed
6564 absWorkingDir, err := filepath.Abs(workingDir)
internal/agent/tools/multiedit.go+2−3
@@ -13,6 +13,7 @@ import (
1313 "charm.land/fantasy"
1414 "github.com/charmbracelet/crush/internal/csync"
1515 "github.com/charmbracelet/crush/internal/diff"
16+ "github.com/charmbracelet/crush/internal/filepathext"
1617 "github.com/charmbracelet/crush/internal/fsext"
1718 "github.com/charmbracelet/crush/internal/history"
1819 "github.com/charmbracelet/crush/internal/lsp"
@@ -62,9 +63,7 @@ func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions pe
6263 return fantasy.NewTextErrorResponse("at least one edit operation is required"), nil
6364 }
6465
65− if !filepath.IsAbs(params.FilePath) {
66− params.FilePath = filepath.Join(workingDir, params.FilePath)
67− }
66+ params.FilePath = filepathext.SmartJoin(workingDir, params.FilePath)
6867
6968 // Validate all edits before applying any
7069 if err := validateEdits(params.Edits); err != nil {
internal/agent/tools/view.go+2−4
@@ -13,6 +13,7 @@ import (
1313
1414 "charm.land/fantasy"
1515 "github.com/charmbracelet/crush/internal/csync"
16+ "github.com/charmbracelet/crush/internal/filepathext"
1617 "github.com/charmbracelet/crush/internal/lsp"
1718 "github.com/charmbracelet/crush/internal/permission"
1819)
@@ -60,10 +61,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
6061 }
6162
6263 // Handle relative paths
63− filePath := params.FilePath
64− if !filepath.IsAbs(filePath) {
65− filePath = filepath.Join(workingDir, filePath)
66− }
64+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
6765
6866 // Check if file is outside working directory and request permission if needed
6967 absWorkingDir, err := filepath.Abs(workingDir)
internal/agent/tools/write.go+2−4
@@ -13,6 +13,7 @@ import (
1313 "charm.land/fantasy"
1414 "github.com/charmbracelet/crush/internal/csync"
1515 "github.com/charmbracelet/crush/internal/diff"
16+ "github.com/charmbracelet/crush/internal/filepathext"
1617 "github.com/charmbracelet/crush/internal/fsext"
1718 "github.com/charmbracelet/crush/internal/history"
1819
@@ -62,10 +63,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
6263 return fantasy.NewTextErrorResponse("content is required"), nil
6364 }
6465
65− filePath := params.FilePath
66− if !filepath.IsAbs(filePath) {
67− filePath = filepath.Join(workingDir, filePath)
68− }
66+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
6967
7068 fileInfo, err := os.Stat(filePath)
7169 if err == nil {
internal/filepathext/filepath.gonew file+27
@@ -0,0 +1,27 @@
1+package filepathext
2+
3+import (
4+ "path/filepath"
5+ "runtime"
6+ "strings"
7+)
8+
9+// SmartJoin joins two paths, treating the second path as absolute if it is an
10+// absolute path.
11+func SmartJoin(one, two string) string {
12+ if SmartIsAbs(two) {
13+ return two
14+ }
15+ return filepath.Join(one, two)
16+}
17+
18+// SmartIsAbs checks if a path is absolute, considering both OS-specific and
19+// Unix-style paths.
20+func SmartIsAbs(path string) bool {
21+ switch runtime.GOOS {
22+ case "windows":
23+ return filepath.IsAbs(path) || strings.HasPrefix(filepath.ToSlash(path), "/")
24+ default:
25+ return filepath.IsAbs(path)
26+ }
27+}