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