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}