@@ -0,0 +1,126 @@
1+package tools
2+
3+import (
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
20+var touchDescription []byte
21+
22+type TouchParams struct {
23+ FilePath string `json:"file_path" description:"The path to the empty file to create"`
24+}
25+
26+type TouchPermissionsParams struct {
27+ FilePath string `json:"file_path"`
28+ OldContent string `json:"old_content,omitempty"`
29+ NewContent string `json:"new_content,omitempty"`
30+}
31+
32+type TouchResponseMetadata struct {
33+ FilePath string `json:"file_path"`
34+}
35+
36+const TouchToolName = "touch"
37+
38+func 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+}