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