write.go 5.2 KB

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