touch.go

  1package tools
  2
  3import (
  4	"context"
  5	_ "embed"
  6	"fmt"
  7	"os"
  8	"path/filepath"
  9
 10	"charm.land/fantasy"
 11	"github.com/charmbracelet/crush/internal/filepathext"
 12	"github.com/charmbracelet/crush/internal/filetracker"
 13	"github.com/charmbracelet/crush/internal/fsext"
 14	"github.com/charmbracelet/crush/internal/history"
 15	"github.com/charmbracelet/crush/internal/lsp"
 16	"github.com/charmbracelet/crush/internal/permission"
 17)
 18
 19//go:embed touch.md
 20var touchDescription []byte
 21
 22type TouchParams struct {
 23	FilePath string `json:"file_path" description:"The path to the empty file to create"`
 24}
 25
 26type TouchPermissionsParams struct {
 27	FilePath   string `json:"file_path"`
 28	OldContent string `json:"old_content,omitempty"`
 29	NewContent string `json:"new_content,omitempty"`
 30}
 31
 32type TouchResponseMetadata struct {
 33	FilePath string `json:"file_path"`
 34}
 35
 36const TouchToolName = "touch"
 37
 38func NewTouchTool(
 39	lspManager *lsp.Manager,
 40	permissions permission.Service,
 41	files history.Service,
 42	filetracker filetracker.Service,
 43	workingDir string,
 44) fantasy.AgentTool {
 45	return fantasy.NewAgentTool(
 46		TouchToolName,
 47		FirstLineDescription(touchDescription),
 48		func(ctx context.Context, params TouchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
 49			if params.FilePath == "" {
 50				return fantasy.NewTextErrorResponse("file_path is required"), nil
 51			}
 52
 53			sessionID := GetSessionFromContext(ctx)
 54			if sessionID == "" {
 55				return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
 56			}
 57
 58			filePath := filepathext.SmartJoin(workingDir, params.FilePath)
 59
 60			fileInfo, err := os.Stat(filePath)
 61			if err == nil {
 62				if fileInfo.IsDir() {
 63					return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
 64				}
 65				return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
 66			} else if !os.IsNotExist(err) {
 67				return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
 68			}
 69
 70			dir := filepath.Dir(filePath)
 71			if err = os.MkdirAll(dir, 0o755); err != nil {
 72				return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
 73			}
 74
 75			p, err := permissions.Request(ctx,
 76				permission.CreatePermissionRequest{
 77					SessionID:   sessionID,
 78					Path:        fsext.PathOrPrefix(filePath, workingDir),
 79					ToolCallID:  call.ID,
 80					ToolName:    TouchToolName,
 81					Action:      "write",
 82					Description: fmt.Sprintf("Create empty file %s", filePath),
 83					Params: TouchPermissionsParams{
 84						FilePath:   filePath,
 85						OldContent: "",
 86						NewContent: "",
 87					},
 88				},
 89			)
 90			if err != nil {
 91				return fantasy.ToolResponse{}, err
 92			}
 93			if !p {
 94				return NewPermissionDeniedResponse(), nil
 95			}
 96
 97			file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
 98			if err != nil {
 99				if os.IsExist(err) {
100					return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
101				}
102				return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
103			}
104			if err = file.Close(); err != nil {
105				return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
106			}
107
108			_, err = files.Create(ctx, sessionID, filePath, "")
109			if err != nil {
110				return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
111			}
112
113			filetracker.RecordRead(ctx, sessionID, filePath)
114
115			notifyLSPs(ctx, lspManager, filePath)
116
117			result := fmt.Sprintf("Empty file successfully created: %s", filePath)
118			result = fmt.Sprintf("<result>\n%s\n</result>", result)
119			result += getDiagnostics(filePath, lspManager)
120			return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
121				TouchResponseMetadata{
122					FilePath: filePath,
123				},
124			), nil
125		})
126}