on main help sign in
fix(tools): fix handling of abs paths on windows
019b7a2Andrey Nering
internal/agent/tools/download.go+2−7
@@ -12,6 +12,7 @@ import (
12 "time"
12 "time"
13
13
14 "charm.land/fantasy"
14 "charm.land/fantasy"
15+ "github.com/charmbracelet/crush/internal/filepathext"
15 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/permission"
16)
17)
17
18
@@ -59,13 +60,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
59 return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
60 return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
60 }
61 }
61
62
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)
63+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
68− }
69
64
70 sessionID := GetSessionFromContext(ctx)
65 sessionID := GetSessionFromContext(ctx)
71 if sessionID == "" {
66 if sessionID == "" {
internal/agent/tools/edit.go+2−3
@@ -13,6 +13,7 @@ import (
13 "charm.land/fantasy"
13 "charm.land/fantasy"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
16+ "github.com/charmbracelet/crush/internal/filepathext"
16 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/history"
18 "github.com/charmbracelet/crush/internal/history"
18
19
@@ -61,9 +62,7 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
61 return fantasy.NewTextErrorResponse("file_path is required"), nil
62 return fantasy.NewTextErrorResponse("file_path is required"), nil
62 }
63 }
63
64
64− if !filepath.IsAbs(params.FilePath) {
65− params.FilePath = filepath.Join(workingDir, params.FilePath)
65+ params.FilePath = filepathext.SmartJoin(workingDir, params.FilePath)
66− }
67
66
68 var response fantasy.ToolResponse
67 var response fantasy.ToolResponse
69 var err error
68 var err error
internal/agent/tools/ls.go+2−3
@@ -11,6 +11,7 @@ import (
11
11
12 "charm.land/fantasy"
12 "charm.land/fantasy"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14+ "github.com/charmbracelet/crush/internal/filepathext"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/permission"
16)
17)
@@ -57,9 +58,7 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
57 return fantasy.NewTextErrorResponse(fmt.Sprintf("error expanding path: %v", err)), nil
58 return fantasy.NewTextErrorResponse(fmt.Sprintf("error expanding path: %v", err)), nil
58 }
59 }
59
60
60− if !filepath.IsAbs(searchPath) {
61− searchPath = filepath.Join(workingDir, searchPath)
61+ searchPath = filepathext.SmartJoin(workingDir, searchPath)
62− }
63
62
64 // Check if directory is outside working directory and request permission if needed
63 // Check if directory is outside working directory and request permission if needed
65 absWorkingDir, err := filepath.Abs(workingDir)
64 absWorkingDir, err := filepath.Abs(workingDir)
internal/agent/tools/multiedit.go+2−3
@@ -13,6 +13,7 @@ import (
13 "charm.land/fantasy"
13 "charm.land/fantasy"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
16+ "github.com/charmbracelet/crush/internal/filepathext"
16 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/history"
18 "github.com/charmbracelet/crush/internal/history"
18 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/lsp"
@@ -62,9 +63,7 @@ func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions pe
62 return fantasy.NewTextErrorResponse("at least one edit operation is required"), nil
63 return fantasy.NewTextErrorResponse("at least one edit operation is required"), nil
63 }
64 }
64
65
65− if !filepath.IsAbs(params.FilePath) {
66− params.FilePath = filepath.Join(workingDir, params.FilePath)
66+ params.FilePath = filepathext.SmartJoin(workingDir, params.FilePath)
67− }
68
67
69 // Validate all edits before applying any
68 // Validate all edits before applying any
70 if err := validateEdits(params.Edits); err != nil {
69 if err := validateEdits(params.Edits); err != nil {
internal/agent/tools/view.go+2−4
@@ -13,6 +13,7 @@ import (
13
13
14 "charm.land/fantasy"
14 "charm.land/fantasy"
15 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/csync"
16+ "github.com/charmbracelet/crush/internal/filepathext"
16 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/permission"
18 "github.com/charmbracelet/crush/internal/permission"
18)
19)
@@ -60,10 +61,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
60 }
61 }
61
62
62 // Handle relative paths
63 // Handle relative paths
63− filePath := params.FilePath
64+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
64− if !filepath.IsAbs(filePath) {
65− filePath = filepath.Join(workingDir, filePath)
66− }
67
65
68 // Check if file is outside working directory and request permission if needed
66 // Check if file is outside working directory and request permission if needed
69 absWorkingDir, err := filepath.Abs(workingDir)
67 absWorkingDir, err := filepath.Abs(workingDir)
internal/agent/tools/write.go+2−4
@@ -13,6 +13,7 @@ import (
13 "charm.land/fantasy"
13 "charm.land/fantasy"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
16+ "github.com/charmbracelet/crush/internal/filepathext"
16 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/history"
18 "github.com/charmbracelet/crush/internal/history"
18
19
@@ -62,10 +63,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
62 return fantasy.NewTextErrorResponse("content is required"), nil
63 return fantasy.NewTextErrorResponse("content is required"), nil
63 }
64 }
64
65
65− filePath := params.FilePath
66+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
66− if !filepath.IsAbs(filePath) {
67− filePath = filepath.Join(workingDir, filePath)
68− }
69
67
70 fileInfo, err := os.Stat(filePath)
68 fileInfo, err := os.Stat(filePath)
71 if err == nil {
69 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+}