/ internal / tools / file_write.go
file_write.go
 1  package tools
 2  
 3  import (
 4  	"context"
 5  	"encoding/json"
 6  	"errors"
 7  	"fmt"
 8  	"os"
 9  	"path/filepath"
10  
11  	"github.com/Kocoro-lab/ShanClaw/internal/agent"
12  	"github.com/Kocoro-lab/ShanClaw/internal/cwdctx"
13  )
14  
15  type FileWriteTool struct{}
16  
17  type fileWriteArgs struct {
18  	Path    string `json:"path"`
19  	Content string `json:"content"`
20  }
21  
22  func (t *FileWriteTool) Info() agent.ToolInfo {
23  	return agent.ToolInfo{
24  		Name:        "file_write",
25  		Description: "Write complete content to a file (overwrites entirely). Use for creating new files or as fallback when file_edit fails due to non-unique text. Always file_read first if the file already exists.",
26  		Parameters: map[string]any{
27  			"type": "object",
28  			"properties": map[string]any{
29  				"path":    map[string]any{"type": "string", "description": "File path to write"},
30  				"content": map[string]any{"type": "string", "description": "Content to write"},
31  			},
32  		},
33  		Required: []string{"path", "content"},
34  	}
35  }
36  
37  func (t *FileWriteTool) Run(ctx context.Context, argsJSON string) (agent.ToolResult, error) {
38  	var args fileWriteArgs
39  	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
40  		return agent.ToolResult{Content: fmt.Sprintf("invalid arguments: %v", err), IsError: true}, nil
41  	}
42  	resolved, resolveErr := cwdctx.ResolveFilesystemPath(ctx, args.Path)
43  	if resolveErr != nil {
44  		if errors.Is(resolveErr, cwdctx.ErrNoSessionCWD) {
45  			return agent.ValidationError(
46  				"file_write: no session working directory is set. Pass an absolute path.",
47  			), nil
48  		}
49  		return agent.ValidationError(fmt.Sprintf("file_write: %v", resolveErr)), nil
50  	}
51  	args.Path = resolved
52  
53  	// Block file_write on the agent's MEMORY.md — always use memory_append.
54  	// Check unconditionally (not just for existing files) so first-write
55  	// scenarios also go through the flock-protected bounded-append path.
56  	if agent.IsMemoryFile(ctx, args.Path) {
57  		return agent.ToolResult{
58  			Content: "Cannot write MEMORY.md with file_write — use the memory_append tool instead.",
59  			IsError: true,
60  		}, nil
61  	}
62  
63  	// Enforce read-before-write for existing files (new files are fine)
64  	if _, err := os.Stat(args.Path); err == nil {
65  		if err := agent.CheckReadBeforeWrite(ctx, args.Path); err != nil {
66  			return agent.ToolResult{Content: err.Error(), IsError: true}, nil
67  		}
68  	}
69  
70  	if err := os.MkdirAll(filepath.Dir(args.Path), 0755); err != nil {
71  		if os.IsPermission(err) {
72  			return agent.PermissionError(fmt.Sprintf("cannot create directory %s: permission denied", filepath.Dir(args.Path))), nil
73  		}
74  		return agent.ToolResult{Content: fmt.Sprintf("error creating directory: %v", err), IsError: true}, nil
75  	}
76  
77  	if err := os.WriteFile(args.Path, []byte(args.Content), 0644); err != nil {
78  		if os.IsPermission(err) {
79  			return agent.PermissionError(fmt.Sprintf("cannot write %s: permission denied", args.Path)), nil
80  		}
81  		return agent.ToolResult{Content: fmt.Sprintf("error writing file: %v", err), IsError: true}, nil
82  	}
83  
84  	return agent.ToolResult{Content: fmt.Sprintf("wrote %d bytes to %s", len(args.Content), args.Path)}, nil
85  }
86  
87  func (t *FileWriteTool) RequiresApproval() bool { return true }
88  
89  func (t *FileWriteTool) IsReadOnlyCall(string) bool { return false }