write.go 6.6 KB

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