write.go 5.5 KB

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