write.go 5.3 KB

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