1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10
11 "github.com/patriceckhart/zot/packages/core"
12 "github.com/patriceckhart/zot/packages/provider"
13)
14
15// WriteTool writes content to a file, creating parent directories.
16type WriteTool struct {
17 CWD string
18 Sandbox *Sandbox
19}
20
21type writeArgs struct {
22 Path string `json:"path"`
23 Content string `json:"content"`
24}
25
26const writeSchema = `{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}`
27
28func (t *WriteTool) Name() string { return "write" }
29func (t *WriteTool) Description() string {
30 return "Write a file. Creates parent dirs. Overwrites."
31}
32func (t *WriteTool) Schema() json.RawMessage { return json.RawMessage(writeSchema) }
33
34func (t *WriteTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
35 var a writeArgs
36 if err := json.Unmarshal(raw, &a); err != nil {
37 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
38 }
39 if a.Path == "" {
40 return core.ToolResult{}, fmt.Errorf("path is required")
41 }
42 path := resolvePath(t.CWD, a.Path)
43 if err := t.Sandbox.CheckWritePath(path); err != nil {
44 return core.ToolResult{}, err
45 }
46
47 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
48 return core.ToolResult{}, err
49 }
50 if err := os.WriteFile(path, []byte(a.Content), 0o644); err != nil {
51 return core.ToolResult{}, err
52 }
53
54 // Return the file content as the result body, just like `read`
55 // does. The TUI renders it with a syntax-highlighted gutter so
56 // the on-screen view after a `write` matches the pre-write
57 // streaming preview seamlessly. The model also sees the written
58 // content in its tool_result, which is useful on follow-up turns
59 // where it wants to reference what it just wrote without a
60 // second `read` call.
61 totalLines := strings.Count(a.Content, "\n")
62 if len(a.Content) > 0 && !strings.HasSuffix(a.Content, "\n") {
63 totalLines++ // count the last unterminated line
64 }
65 return core.ToolResult{
66 Content: []provider.Content{provider.TextBlock{Text: a.Content}},
67 Details: map[string]any{
68 "path": path,
69 "bytes": len(a.Content),
70 "total_lines": totalLines,
71 "start_line": 1,
72 },
73 }, nil
74}