write.go 5.0 KB

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