Detailed changes
@@ -390,6 +390,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
+ tools.NewDeleteTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
tools.NewGlobTool(c.cfg.WorkingDir()),
tools.NewGrepTool(c.cfg.WorkingDir()),
@@ -70,7 +70,7 @@ interactions:
proto_minor: 1
content_length: 51899
host: ""
@@ -64,7 +64,7 @@ interactions:
proto_minor: 1
content_length: 51924
host: ""
@@ -70,7 +70,7 @@ interactions:
proto_minor: 1
content_length: 51942
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51860
host: ""
@@ -70,7 +70,7 @@ interactions:
proto_minor: 1
content_length: 51858
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 51852
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51938
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51949
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51822
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51812
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51872
host: ""
@@ -67,7 +67,7 @@ interactions:
proto_minor: 1
content_length: 51878
host: ""
@@ -8,7 +8,7 @@ interactions:
proto_minor: 1
content_length: 51915
host: ""
@@ -73,7 +73,7 @@ interactions:
proto_minor: 1
content_length: 50240
host: ""
@@ -57,7 +57,7 @@ interactions:
proto_minor: 1
content_length: 50265
host: ""
@@ -63,7 +63,7 @@ interactions:
proto_minor: 1
content_length: 50283
host: ""
@@ -63,7 +63,7 @@ interactions:
proto_minor: 1
content_length: 50201
host: ""
@@ -65,7 +65,7 @@ interactions:
proto_minor: 1
content_length: 50199
host: ""
@@ -59,7 +59,7 @@ interactions:
proto_minor: 1
content_length: 50193
host: ""
@@ -75,7 +75,7 @@ interactions:
proto_minor: 1
content_length: 50279
host: ""
@@ -61,7 +61,7 @@ interactions:
proto_minor: 1
content_length: 50290
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 50163
host: ""
@@ -49,7 +49,7 @@ interactions:
proto_minor: 1
content_length: 50153
host: ""
@@ -71,7 +71,7 @@ interactions:
proto_minor: 1
content_length: 50213
host: ""
@@ -65,7 +65,7 @@ interactions:
proto_minor: 1
content_length: 50219
host: ""
@@ -57,7 +57,7 @@ interactions:
proto_minor: 1
content_length: 50256
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 50389
host: ""
@@ -59,7 +59,7 @@ interactions:
proto_minor: 1
content_length: 50414
host: ""
@@ -59,7 +59,7 @@ interactions:
proto_minor: 1
content_length: 50432
host: ""
@@ -65,7 +65,7 @@ interactions:
proto_minor: 1
content_length: 50350
host: ""
@@ -51,7 +51,7 @@ interactions:
proto_minor: 1
content_length: 50348
host: ""
@@ -8,7 +8,7 @@ interactions:
proto_minor: 1
content_length: 50342
host: ""
@@ -69,7 +69,7 @@ interactions:
proto_minor: 1
content_length: 50428
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 50439
host: ""
@@ -49,7 +49,7 @@ interactions:
proto_minor: 1
content_length: 50312
host: ""
@@ -47,7 +47,7 @@ interactions:
proto_minor: 1
content_length: 50302
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50362
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50368
host: ""
@@ -59,7 +59,7 @@ interactions:
proto_minor: 1
content_length: 50405
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 50229
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50254
host: ""
@@ -8,7 +8,7 @@ interactions:
proto_minor: 1
content_length: 50272
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50190
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50188
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50182
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 50268
host: ""
@@ -53,7 +53,7 @@ interactions:
proto_minor: 1
content_length: 50279
host: ""
@@ -49,7 +49,7 @@ interactions:
proto_minor: 1
content_length: 50152
host: ""
@@ -45,7 +45,7 @@ interactions:
proto_minor: 1
content_length: 50142
host: ""
@@ -55,7 +55,7 @@ interactions:
proto_minor: 1
content_length: 50202
host: ""
@@ -59,7 +59,7 @@ interactions:
proto_minor: 1
content_length: 50208
host: ""
@@ -57,7 +57,7 @@ interactions:
proto_minor: 1
content_length: 50245
host: ""
@@ -137,6 +137,9 @@ var bannedCommands = []string{
"pfctl",
"route",
"ufw",
+
+ // File deletion (use delete tool instead for proper LSP integration)
+ "rm",
}
func bashDescription(attribution *config.Attribution, modelName string) string {
@@ -0,0 +1,130 @@
+package tools
+
+import (
+ "context"
+ _ "embed"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "charm.land/fantasy"
+ "github.com/charmbracelet/crush/internal/csync"
+ "github.com/charmbracelet/crush/internal/filepathext"
+ "github.com/charmbracelet/crush/internal/fsext"
+ "github.com/charmbracelet/crush/internal/lsp"
+ "github.com/charmbracelet/crush/internal/permission"
+ "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
+)
+
+//go:embed delete.md
+var deleteDescription []byte
+
+// DeleteParams defines the parameters for the delete tool.
+type DeleteParams struct {
+ FilePath string `json:"file_path" description:"The path to the file or directory to delete"`
+ Recursive bool `json:"recursive,omitempty" description:"If true, recursively delete directory contents (default false)"`
+}
+
+// DeletePermissionsParams defines the parameters shown in permission requests.
+type DeletePermissionsParams struct {
+ FilePath string `json:"file_path"`
+ Recursive bool `json:"recursive,omitempty"`
+ IsDir bool `json:"is_dir,omitempty"`
+}
+
+// DeleteToolName is the name of the delete tool.
+const DeleteToolName = "delete"
+
+// NewDeleteTool creates a new delete tool.
+func NewDeleteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
+ DeleteToolName,
+ string(deleteDescription),
+ func(ctx context.Context, params DeleteParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
+ if params.FilePath == "" {
+ return fantasy.NewTextErrorResponse("file_path is required"), nil
+ }
+
+ filePath := filepathext.SmartJoin(workingDir, params.FilePath)
+
+ fileInfo, err := os.Stat(filePath)
+ if os.IsNotExist(err) {
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path does not exist: %s", filePath)), nil
+ }
+ if err != nil {
+ return fantasy.ToolResponse{}, fmt.Errorf("error checking path: %w", err)
+ }
+
+ isDir := fileInfo.IsDir()
+
+ if isDir && !params.Recursive {
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Cannot delete directory %s. Set recursive=true to delete directory and its contents.", filePath)), nil
+ }
+
+ sessionID := GetSessionFromContext(ctx)
+ if sessionID == "" {
+ return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
+ }
+
+ p, err := permissions.Request(ctx,
+ permission.CreatePermissionRequest{
+ SessionID: sessionID,
+ Path: fsext.PathOrPrefix(filePath, workingDir),
+ ToolCallID: call.ID,
+ ToolName: DeleteToolName,
+ Action: "delete",
+ Description: buildDeleteDescription(filePath, isDir),
+ Params: DeletePermissionsParams{
+ FilePath: filePath,
+ Recursive: params.Recursive,
+ IsDir: isDir,
+ },
+ },
+ )
+ if err != nil {
+ return fantasy.ToolResponse{}, err
+ }
+ if !p {
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
+ }
+
+ if err := os.RemoveAll(filePath); err != nil {
+ return fantasy.ToolResponse{}, fmt.Errorf("error deleting path: %w", err)
+ }
+
+ lspCloseAndDeleteFiles(ctx, lspClients, filePath)
+ return fantasy.NewTextResponse(fmt.Sprintf("Successfully deleted: %s", filePath)), nil
+ })
+}
+
+func buildDeleteDescription(filePath string, isDir bool) string {
+ if !isDir {
+ return fmt.Sprintf("Delete file %s", filePath)
+ }
+ return fmt.Sprintf("Delete directory %s and all its contents", filePath)
+}
+
+func lspCloseAndDeleteFiles(ctx context.Context, lsps *csync.Map[string, *lsp.Client], filePath string) {
+ cleanPath := filepath.Clean(filePath)
+ for client := range lsps.Seq() {
+ for uri := range client.OpenFiles() {
+ path, err := protocol.DocumentURI(uri).Path()
+ if err != nil {
+ continue
+ }
+ if path != cleanPath && !strings.HasPrefix(path, cleanPath+string(filepath.Separator)) {
+ continue
+ }
+ _ = client.CloseFile(ctx, path)
+ _ = client.DidChangeWatchedFiles(ctx, protocol.DidChangeWatchedFilesParams{
+ Changes: []protocol.FileEvent{
+ {
+ URI: protocol.URIFromPath(path),
+ Type: protocol.Deleted,
+ },
+ },
+ })
+ }
+ }
+}
@@ -0,0 +1,14 @@
+Deletes files or directories from the filesystem.
+
+<usage>
+- Provide file path to delete
+- For directories, always set recursive=true to delete the directory
+- Tool handles LSP cleanup automatically (closes open files, clears diagnostics)
+</usage>
+
+<limitations>
+- Cannot delete files outside the working directory
+- Deleting directories requires recursive=true (even if empty)
+- Deleted files cannot be recovered (no trash/recycle bin)
+</limitations>
+
@@ -685,6 +685,7 @@ func allToolNames() []string {
"bash",
"job_output",
"job_kill",
+ "delete",
"download",
"edit",
"multiedit",
@@ -486,7 +486,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
coderAgent, ok := cfg.Agents[AgentCoder]
require.True(t, ok)
- assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write"}, coderAgent.AllowedTools)
+ assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "delete", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write"}, coderAgent.AllowedTools)
taskAgent, ok := cfg.Agents[AgentTask]
require.True(t, ok)
@@ -509,7 +509,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) {
cfg.SetupAgents()
coderAgent, ok := cfg.Agents[AgentCoder]
require.True(t, ok)
- assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "agentic_fetch", "todos", "write"}, coderAgent.AllowedTools)
+ assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "delete", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "agentic_fetch", "todos", "write"}, coderAgent.AllowedTools)
taskAgent, ok := cfg.Agents[AgentTask]
require.True(t, ok)
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
+ "iter"
"log/slog"
"maps"
"os"
@@ -336,6 +337,35 @@ func (c *Client) IsFileOpen(filepath string) bool {
return exists
}
+// CloseFile closes a single file in the LSP server and clears its diagnostics.
+func (c *Client) CloseFile(ctx context.Context, filepath string) error {
+ uri := string(protocol.URIFromPath(filepath))
+
+ if _, exists := c.openFiles.Get(uri); !exists {
+ return nil // Not open, nothing to do.
+ }
+
+ if err := c.client.NotifyDidCloseTextDocument(ctx, uri); err != nil {
+ return fmt.Errorf("error closing file: %w", err)
+ }
+
+ c.openFiles.Del(uri)
+ c.ClearDiagnosticsForURI(protocol.DocumentURI(uri))
+
+ return nil
+}
+
+// OpenFiles returns an iterator over all currently open file URIs.
+func (c *Client) OpenFiles() iter.Seq[string] {
+ return func(yield func(string) bool) {
+ for uri := range c.openFiles.Seq2() {
+ if !yield(uri) {
+ return
+ }
+ }
+ }
+}
+
// CloseAllFiles closes all currently open files.
func (c *Client) CloseAllFiles(ctx context.Context) {
cfg := config.Get()