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 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), &params); 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}