summaryrefslogtreecommitdiff
path: root/agent/tools/write.lua
blob: 01bf3b2699c68eb911a0042c2ae5ce17cdb119d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
-- Write text to a file, creating parent directories as needed. Keep the
-- description terse because every byte of every tool description rides
-- in the system prompt on every turn.

local uv = require("luv")

-- mkdir -p equivalent: walk the path components and create each.
-- Ignores EEXIST so the call is idempotent.
local function mkdir_p(path)
    if path == "" or path == "." or path == "/" then return true end
    local parts = {}
    for piece in string.gmatch(path, "[^/]+") do
        parts[#parts + 1] = piece
    end
    local cur = (path:sub(1, 1) == "/") and "" or "."
    for _, piece in ipairs(parts) do
        cur = cur .. "/" .. piece
        local ok, err, name = uv.fs_mkdir(cur, tonumber("755", 8))
        if not ok and name ~= "EEXIST" then
            return nil, err
        end
    end
    return true
end

-- Return the dirname of `path`, or nil if there isn't one (basename only).
local function dirname(path)
    local slash = path:find("/[^/]*$")
    if not slash or slash == 1 then return nil end
    return path:sub(1, slash - 1)
end

return {
    name = "std.write",
    description = "Save content to a path. Overwrites existing files; missing parent directories are created automatically.",
    schema = {
        type = "object",
        properties = {
            path = { type = "string", description = "Path to the file (relative or absolute)." },
            content = { type = "string", description = "Exact bytes to write. No newline is added." },
        },
        required = { "path", "content" },
    },
    handler = function(input)
        local path = input.path
        local content = input.content

        if type(path) ~= "string" or path == "" then
            return "Error: `path` must be a non-empty string."
        end
        if type(content) ~= "string" then
            return "Error: `content` must be a string."
        end

        local parent = dirname(path)
        if parent then
            local ok, err = mkdir_p(parent)
            if not ok then
                return "Error: could not create parent directory " .. parent .. ": " .. tostring(err)
            end
        end

        local f, open_err = io.open(path, "wb")
        if not f then
            return "Error: " .. (open_err or ("could not open " .. path .. " for writing"))
        end

        local ok, write_err = f:write(content)
        if not ok then
            f:close()
            return "Error: write failed: " .. tostring(write_err)
        end

        local close_ok, close_err = f:close()
        if not close_ok then
            return "Error: close failed: " .. tostring(close_err)
        end

        return string.format("Wrote %d bytes to %s.", #content, path)
    end,
}