package builtin import ( "context" "encoding/json" "fmt" "os" "strings" "nub/internal/tool" ) type EditTool struct{} func (EditTool) Name() string { return "edit" } func (EditTool) Description() string { return "Ersetzt einen exakten String in einer Datei. Bei mehreren Treffern ohne replace_all: Fehler." } func (EditTool) Schema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "path": {"type": "string"}, "old_string": {"type": "string"}, "new_string": {"type": "string"}, "replace_all": {"type": "boolean"} }, "required": ["path", "old_string", "new_string"] }`) } type editInput struct { Path string `json:"path"` OldString string `json:"old_string"` NewString string `json:"new_string"` ReplaceAll bool `json:"replace_all"` } func (EditTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) { var in editInput if err := json.Unmarshal(input, &in); err != nil { return tool.Result{ForModel: "invalid input: " + err.Error(), IsError: true}, nil } if in.OldString == in.NewString { return tool.Result{ForModel: "old_string and new_string are identical", IsError: true}, nil } path, err := resolvePath(env.RepoRoot, env.Cwd, in.Path) if err != nil { return tool.Result{ForModel: err.Error(), IsError: true}, nil } data, err := os.ReadFile(path) if err != nil { return tool.Result{ForModel: fmt.Sprintf("read %s: %v", in.Path, err), IsError: true}, nil } old := string(data) count := strings.Count(old, in.OldString) if count == 0 { return tool.Result{ForModel: fmt.Sprintf("%s: old_string not found", in.Path), IsError: true}, nil } if count > 1 && !in.ReplaceAll { return tool.Result{ForModel: fmt.Sprintf("%s: old_string is ambiguous (%d matches); use replace_all or a more specific old_string", in.Path, count), IsError: true}, nil } var next string if in.ReplaceAll { next = strings.ReplaceAll(old, in.OldString, in.NewString) } else { next = strings.Replace(old, in.OldString, in.NewString, 1) } if err := os.WriteFile(path, []byte(next), 0o644); err != nil { return tool.Result{ForModel: fmt.Sprintf("write %s: %v", in.Path, err), IsError: true}, nil } replaced := 1 if in.ReplaceAll { replaced = count } return tool.Result{ ForModel: fmt.Sprintf("%s: %d replacement(s)", in.Path, replaced), ForUI: DiffResult{Path: in.Path, Lines: lineDiff(old, next)}, }, nil }