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