write.go

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