diff --git a/internal/agent/tools/download.go b/internal/agent/tools/download.go index cae5e5d6ab2f64f5e62521bf1f9379411dfbe0ad..9c2fbe91441bfd5658982c4e2cbd8bc628c4fc2c 100644 --- a/internal/agent/tools/download.go +++ b/internal/agent/tools/download.go @@ -12,6 +12,7 @@ import ( "time" "charm.land/fantasy" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/permission" ) @@ -59,13 +60,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client * return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil } - // Convert relative path to absolute path - var filePath string - if filepath.IsAbs(params.FilePath) { - filePath = params.FilePath - } else { - filePath = filepath.Join(workingDir, params.FilePath) - } + filePath := filepathext.SmartJoin(workingDir, params.FilePath) sessionID := GetSessionFromContext(ctx) if sessionID == "" { diff --git a/internal/agent/tools/edit.go b/internal/agent/tools/edit.go index bcd6592f7c0693433bebcb4a76432269df1e2191..7012afc8f525a39c2c431ec7327a9fb2d378ef42 100644 --- a/internal/agent/tools/edit.go +++ b/internal/agent/tools/edit.go @@ -13,6 +13,7 @@ import ( "charm.land/fantasy" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/diff" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/history" @@ -61,9 +62,7 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss return fantasy.NewTextErrorResponse("file_path is required"), nil } - if !filepath.IsAbs(params.FilePath) { - params.FilePath = filepath.Join(workingDir, params.FilePath) - } + params.FilePath = filepathext.SmartJoin(workingDir, params.FilePath) var response fantasy.ToolResponse var err error diff --git a/internal/agent/tools/ls.go b/internal/agent/tools/ls.go index 21ad0cbc80c51d253d391306a5a1621dd3c29671..7725910c11675c941d791a6ec5d57a190535ee9e 100644 --- a/internal/agent/tools/ls.go +++ b/internal/agent/tools/ls.go @@ -11,6 +11,7 @@ import ( "charm.land/fantasy" "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/permission" ) @@ -57,9 +58,7 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi return fantasy.NewTextErrorResponse(fmt.Sprintf("error expanding path: %v", err)), nil } - if !filepath.IsAbs(searchPath) { - searchPath = filepath.Join(workingDir, searchPath) - } + searchPath = filepathext.SmartJoin(workingDir, searchPath) // Check if directory is outside working directory and request permission if needed absWorkingDir, err := filepath.Abs(workingDir) diff --git a/internal/agent/tools/multiedit.go b/internal/agent/tools/multiedit.go index a0f6d3c83decde013a829b4c4fe13902545bc9db..82df1824244e67553c037f4f5b38311ff37f688e 100644 --- a/internal/agent/tools/multiedit.go +++ b/internal/agent/tools/multiedit.go @@ -13,6 +13,7 @@ import ( "charm.land/fantasy" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/diff" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/history" "github.com/charmbracelet/crush/internal/lsp" @@ -62,9 +63,7 @@ func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions pe return fantasy.NewTextErrorResponse("at least one edit operation is required"), nil } - if !filepath.IsAbs(params.FilePath) { - params.FilePath = filepath.Join(workingDir, params.FilePath) - } + params.FilePath = filepathext.SmartJoin(workingDir, params.FilePath) // Validate all edits before applying any if err := validateEdits(params.Edits); err != nil { diff --git a/internal/agent/tools/view.go b/internal/agent/tools/view.go index af9212acd9f9207a8e1b82b0f7e11aa2a1618994..cde12c2a76f49e08e38ecb26ea1c9d707c83a597 100644 --- a/internal/agent/tools/view.go +++ b/internal/agent/tools/view.go @@ -13,6 +13,7 @@ import ( "charm.land/fantasy" "github.com/charmbracelet/crush/internal/csync" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/permission" ) @@ -60,10 +61,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss } // Handle relative paths - filePath := params.FilePath - if !filepath.IsAbs(filePath) { - filePath = filepath.Join(workingDir, filePath) - } + filePath := filepathext.SmartJoin(workingDir, params.FilePath) // Check if file is outside working directory and request permission if needed absWorkingDir, err := filepath.Abs(workingDir) diff --git a/internal/agent/tools/write.go b/internal/agent/tools/write.go index 6c62fb0d5c2a9454a02300c22b69a7a20574c470..0868b9f62306e7b43b1c7218ff68a6a7aae140eb 100644 --- a/internal/agent/tools/write.go +++ b/internal/agent/tools/write.go @@ -13,6 +13,7 @@ import ( "charm.land/fantasy" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/diff" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/history" @@ -62,10 +63,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis return fantasy.NewTextErrorResponse("content is required"), nil } - filePath := params.FilePath - if !filepath.IsAbs(filePath) { - filePath = filepath.Join(workingDir, filePath) - } + filePath := filepathext.SmartJoin(workingDir, params.FilePath) fileInfo, err := os.Stat(filePath) if err == nil { diff --git a/internal/filepathext/filepath.go b/internal/filepathext/filepath.go new file mode 100644 index 0000000000000000000000000000000000000000..08aac0c7cc15208eb263662589a80614a4d46ab5 --- /dev/null +++ b/internal/filepathext/filepath.go @@ -0,0 +1,27 @@ +package filepathext + +import ( + "path/filepath" + "runtime" + "strings" +) + +// SmartJoin joins two paths, treating the second path as absolute if it is an +// absolute path. +func SmartJoin(one, two string) string { + if SmartIsAbs(two) { + return two + } + return filepath.Join(one, two) +} + +// SmartIsAbs checks if a path is absolute, considering both OS-specific and +// Unix-style paths. +func SmartIsAbs(path string) bool { + switch runtime.GOOS { + case "windows": + return filepath.IsAbs(path) || strings.HasPrefix(filepath.ToSlash(path), "/") + default: + return filepath.IsAbs(path) + } +}