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