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 p := permission.Default.Request(
105 permission.CreatePermissionRequest{
106 Path: filePath,
107 ToolName: WriteToolName,
108 Action: "create",
109 Description: fmt.Sprintf("Create file %s", filePath),
110 Params: WritePermissionsParams{
111 FilePath: filePath,
112 Content: GenerateDiff("", params.Content),
113 },
114 },
115 )
116 if !p {
117 return NewTextErrorResponse(fmt.Sprintf("Permission denied to create file: %s", filePath)), nil
118 }
119
120 // Write the file
121 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
122 if err != nil {
123 return NewTextErrorResponse(fmt.Sprintf("Failed to write file: %s", err)), nil
124 }
125
126 // Record the file write
127 recordFileWrite(filePath)
128 recordFileRead(filePath)
129
130 result := fmt.Sprintf("File successfully written: %s", filePath)
131 result = fmt.Sprintf("<result>\n%s\n</result>", result)
132 result += appendDiagnostics(filePath, w.lspClients)
133 return NewTextResponse(result), nil
134}
135
136func writeDescription() string {
137 return `File writing tool that creates or updates files in the filesystem, allowing you to save or modify text content.
138
139WHEN TO USE THIS TOOL:
140- Use when you need to create a new file
141- Helpful for updating existing files with modified content
142- Perfect for saving generated code, configurations, or text data
143
144HOW TO USE:
145- Provide the path to the file you want to write
146- Include the content to be written to the file
147- The tool will create any necessary parent directories
148
149FEATURES:
150- Can create new files or overwrite existing ones
151- Creates parent directories automatically if they don't exist
152- Checks if the file has been modified since last read for safety
153- Avoids unnecessary writes when content hasn't changed
154
155LIMITATIONS:
156- You should read a file before writing to it to avoid conflicts
157- Cannot append to files (rewrites the entire file)
158
159
160TIPS:
161- Use the View tool first to examine existing files before modifying them
162- Use the LS tool to verify the correct location when creating new files
163- Combine with Glob and Grep tools to find and modify multiple files
164- Always include descriptive comments when making changes to existing code`
165}
166
167func NewWriteTool(lspClients map[string]*lsp.Client) BaseTool {
168 return &writeTool{
169 lspClients,
170 }
171}