1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "time"
10
11 "github.com/kujtimiihoxha/termai/internal/config"
12 "github.com/kujtimiihoxha/termai/internal/lsp"
13 "github.com/kujtimiihoxha/termai/internal/permission"
14)
15
16type writeTool struct {
17 lspClients map[string]*lsp.Client
18}
19
20const (
21 WriteToolName = "write"
22)
23
24type WriteParams struct {
25 FilePath string `json:"file_path"`
26 Content string `json:"content"`
27}
28
29type WritePermissionsParams struct {
30 FilePath string `json:"file_path"`
31 Content string `json:"content"`
32}
33
34func (w *writeTool) Info() ToolInfo {
35 return ToolInfo{
36 Name: WriteToolName,
37 Description: writeDescription(),
38 Parameters: map[string]any{
39 "file_path": map[string]any{
40 "type": "string",
41 "description": "The path to the file to write",
42 },
43 "content": map[string]any{
44 "type": "string",
45 "description": "The content to write to the file",
46 },
47 },
48 Required: []string{"file_path", "content"},
49 }
50}
51
52// Run implements Tool.
53func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
54 var params WriteParams
55 if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil {
56 return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
57 }
58
59 if params.FilePath == "" {
60 return NewTextErrorResponse("file_path is required"), nil
61 }
62
63 if params.Content == "" {
64 return NewTextErrorResponse("content is required"), nil
65 }
66
67 // Handle relative paths
68 filePath := params.FilePath
69 if !filepath.IsAbs(filePath) {
70 filePath = filepath.Join(config.WorkingDirectory(), filePath)
71 }
72
73 // Check if file exists and is a directory
74 fileInfo, err := os.Stat(filePath)
75 if err == nil {
76 if fileInfo.IsDir() {
77 return NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
78 }
79
80 // Check if file was modified since last read
81 modTime := fileInfo.ModTime()
82 lastRead := getLastReadTime(filePath)
83 if modTime.After(lastRead) {
84 return NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
85 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
86 }
87
88 // Optional: Get old content for diff
89 oldContent, readErr := os.ReadFile(filePath)
90 if readErr == nil && string(oldContent) == params.Content {
91 return NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
92 }
93 } else if !os.IsNotExist(err) {
94 return NewTextErrorResponse(fmt.Sprintf("Failed to access file: %s", err)), nil
95 }
96
97 // Create parent directories if needed
98 dir := filepath.Dir(filePath)
99 if err = os.MkdirAll(dir, 0o755); err != nil {
100 return NewTextErrorResponse(fmt.Sprintf("Failed to create parent directories: %s", err)), nil
101 }
102
103 notifyLspOpenFile(ctx, filePath, w.lspClients)
104 // Get old content for diff if file exists
105 oldContent := ""
106 if fileInfo != nil && !fileInfo.IsDir() {
107 oldBytes, readErr := os.ReadFile(filePath)
108 if readErr == nil {
109 oldContent = string(oldBytes)
110 }
111 }
112
113 p := permission.Default.Request(
114 permission.CreatePermissionRequest{
115 Path: filePath,
116 ToolName: WriteToolName,
117 Action: "create",
118 Description: fmt.Sprintf("Create file %s", filePath),
119 Params: WritePermissionsParams{
120 FilePath: filePath,
121 Content: GenerateDiff(oldContent, params.Content),
122 },
123 },
124 )
125 if !p {
126 return NewTextErrorResponse(fmt.Sprintf("Permission denied to create file: %s", filePath)), nil
127 }
128
129 // Write the file
130 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
131 if err != nil {
132 return NewTextErrorResponse(fmt.Sprintf("Failed to write file: %s", err)), nil
133 }
134
135 // Record the file write
136 recordFileWrite(filePath)
137 recordFileRead(filePath)
138
139 result := fmt.Sprintf("File successfully written: %s", filePath)
140 result = fmt.Sprintf("<result>\n%s\n</result>", result)
141 result += appendDiagnostics(filePath, w.lspClients)
142 return NewTextResponse(result), nil
143}
144
145func writeDescription() string {
146 return `File writing tool that creates or updates files in the filesystem, allowing you to save or modify text content.
147
148WHEN TO USE THIS TOOL:
149- Use when you need to create a new file
150- Helpful for updating existing files with modified content
151- Perfect for saving generated code, configurations, or text data
152
153HOW TO USE:
154- Provide the path to the file you want to write
155- Include the content to be written to the file
156- The tool will create any necessary parent directories
157
158FEATURES:
159- Can create new files or overwrite existing ones
160- Creates parent directories automatically if they don't exist
161- Checks if the file has been modified since last read for safety
162- Avoids unnecessary writes when content hasn't changed
163
164LIMITATIONS:
165- You should read a file before writing to it to avoid conflicts
166- Cannot append to files (rewrites the entire file)
167
168
169TIPS:
170- Use the View tool first to examine existing files before modifying them
171- Use the LS tool to verify the correct location when creating new files
172- Combine with Glob and Grep tools to find and modify multiple files
173- Always include descriptive comments when making changes to existing code`
174}
175
176func NewWriteTool(lspClients map[string]*lsp.Client) BaseTool {
177 return &writeTool{
178 lspClients,
179 }
180}