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