108 lines · 4.1 KB · Lua
RawDownload
-- plugins/edit_file.lua-- A str_replace-style file editing tool.-- The model specifies an exact string to find and its replacement. clm.tool_register("edit_file", {    description = "Edit a file by replacing an exact string with new content. "        .. "The old_str must match exactly (including whitespace and indentation). "        .. "By default old_str must match exactly once; pass replace_all=true to "        .. "replace every occurrence instead (e.g. renaming a symbol, or a "        .. "mechanical single-character substitution across a whole file) rather "        .. "than retyping surrounding text by hand, which risks transcription "        .. "errors. Use this for surgical edits; use write_file for creating new files.",    params_schema = {        type = "object",        properties = {            path = {                type = "string",                description = "path to the file to edit",            },            old_str = {                type = "string",                description = "exact string to find in the file (must match "                    .. "uniquely unless replace_all is true)",            },            new_str = {                type = "string",                description = "replacement string (empty string to delete)",            },            replace_all = {                type = "boolean",                description = "replace every non-overlapping occurrence of "                    .. "old_str instead of requiring exactly one match "                    .. "(default false)",            },        },        required = { "path", "old_str", "new_str" },    },    invoke = function(args, ctx)        if not args.path or args.path == "" then            ctx:fail("missing 'path'")            return        end        if not args.old_str or args.old_str == "" then            ctx:fail("missing 'old_str' (use write_file to create new files)")            return        end        if args.new_str == nil then            ctx:fail("missing 'new_str'")            return        end         local content, err = clm.read_file(args.path)        if not content then            ctx:fail("cannot read '" .. args.path .. "': " .. (err or "unknown"))            return        end         -- Count non-overlapping occurrences (matches the replacement pass        -- below, so the failure message and the eventual "replaced N"        -- message always agree even for a self-overlapping old_str).        local count = 0        local pos = 1        while true do            local i = string.find(content, args.old_str, pos, true)            if not i then break end            count = count + 1            pos = i + #args.old_str        end         if count == 0 then            ctx:fail("old_str not found in " .. args.path)            return        end        if count > 1 and not args.replace_all then            ctx:fail("old_str matches " .. tostring(count)                .. " times; must be unique (pass replace_all=true to replace "                .. "all occurrences, or add more surrounding context to "                .. "old_str to target just one)")            return        end         -- Perform the replacement(s), left to right, non-overlapping.        local parts = {}        local n = 0        pos = 1        while true do            local i = string.find(content, args.old_str, pos, true)            if not i then break end            parts[#parts + 1] = string.sub(content, pos, i - 1)            parts[#parts + 1] = args.new_str            pos = i + #args.old_str            n = n + 1            if not args.replace_all then break end        end        parts[#parts + 1] = string.sub(content, pos)        local new_content = table.concat(parts)         local ok, werr = clm.write_file(args.path, new_content)        if not ok then            ctx:fail("cannot write '" .. args.path .. "': " .. (werr or "unknown"))            return        end         ctx:complete("ok: replaced " .. tostring(n) .. " occurrence(s) of "            .. tostring(#args.old_str) .. " bytes with "            .. tostring(#args.new_str) .. " bytes each in " .. args.path)    end,})