2026-02-26 17:41:02 +03:00
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2026-03-16 11:10:05 +03:00
|
|
|
"strings"
|
2026-02-26 17:41:02 +03:00
|
|
|
|
|
|
|
|
"charm.land/fantasy"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type writeArgs struct {
|
|
|
|
|
Path string `json:"path"`
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewWriteTool creates the write core tool.
|
2026-02-27 11:37:46 +03:00
|
|
|
func NewWriteTool(opts ...ToolOption) fantasy.AgentTool {
|
|
|
|
|
cfg := ApplyOptions(opts)
|
2026-02-26 17:41:02 +03:00
|
|
|
return &coreTool{
|
|
|
|
|
info: fantasy.ToolInfo{
|
|
|
|
|
Name: "write",
|
|
|
|
|
Description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
|
|
|
|
Parameters: map[string]any{
|
|
|
|
|
"path": map[string]any{
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Path to the file to write (relative or absolute)",
|
|
|
|
|
},
|
|
|
|
|
"content": map[string]any{
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Content to write to the file",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Required: []string{"path", "content"},
|
|
|
|
|
},
|
2026-02-27 11:37:46 +03:00
|
|
|
handler: func(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
|
|
|
|
return executeWrite(ctx, call, cfg.WorkDir)
|
|
|
|
|
},
|
2026-02-26 17:41:02 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 11:37:46 +03:00
|
|
|
func executeWrite(ctx context.Context, call fantasy.ToolCall, workDir string) (fantasy.ToolResponse, error) {
|
2026-06-06 19:22:05 +03:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return fantasy.ToolResponse{}, err
|
|
|
|
|
}
|
2026-02-26 17:41:02 +03:00
|
|
|
var args writeArgs
|
|
|
|
|
if err := parseArgs(call.Input, &args); err != nil {
|
|
|
|
|
return fantasy.NewTextErrorResponse("path and content parameters are required"), nil
|
|
|
|
|
}
|
|
|
|
|
if args.Path == "" {
|
|
|
|
|
return fantasy.NewTextErrorResponse("path parameter is required"), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 11:37:46 +03:00
|
|
|
absPath, err := resolvePathWithWorkDir(args.Path, workDir)
|
2026-02-26 17:41:02 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid path: %v", err)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 11:10:05 +03:00
|
|
|
// Read existing content before writing (for diff metadata).
|
|
|
|
|
var beforeContent string
|
|
|
|
|
isNew := true
|
|
|
|
|
if existing, readErr := os.ReadFile(absPath); readErr == nil {
|
|
|
|
|
beforeContent = string(existing)
|
|
|
|
|
isNew = false
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 17:41:02 +03:00
|
|
|
// Create parent directories
|
|
|
|
|
dir := filepath.Dir(absPath)
|
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to create directories: %v", err)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.WriteFile(absPath, []byte(args.Content), 0644); err != nil {
|
|
|
|
|
return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to write file: %v", err)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 11:10:05 +03:00
|
|
|
resp := fantasy.NewTextResponse(fmt.Sprintf("Wrote %d bytes to %s", len(args.Content), args.Path))
|
|
|
|
|
return fantasy.WithResponseMetadata(resp, writeDiffMeta(absPath, beforeContent, args.Content, isNew)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeDiffMeta builds the structured metadata attached to write tool responses.
|
|
|
|
|
func writeDiffMeta(path, beforeContent, afterContent string, isNew bool) map[string]any {
|
|
|
|
|
additions := strings.Count(afterContent, "\n") + 1
|
|
|
|
|
deletions := 0
|
|
|
|
|
if !isNew {
|
|
|
|
|
deletions = strings.Count(beforeContent, "\n") + 1
|
|
|
|
|
}
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"file_diffs": []map[string]any{{
|
|
|
|
|
"path": path,
|
|
|
|
|
"additions": additions,
|
|
|
|
|
"deletions": deletions,
|
|
|
|
|
"is_new": isNew,
|
|
|
|
|
"diff_blocks": []map[string]any{{
|
|
|
|
|
"old_text": beforeContent,
|
|
|
|
|
"new_text": afterContent,
|
|
|
|
|
}},
|
|
|
|
|
}},
|
|
|
|
|
}
|
2026-02-26 17:41:02 +03:00
|
|
|
}
|