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