write.go 5.6 KB

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