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