local std = require("evalframe.std")
local M = {}
local function shell_escape(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
local function env_strip_prefix()
local vars = { "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SSE_PORT" }
local parts = {}
for _, v in ipairs(vars) do
parts[#parts + 1] = string.format("unset %s;", v)
end
return table.concat(parts, " ") .. " "
end
local function cli_call(prompt, opts)
opts = opts or {}
local parts = { env_strip_prefix() }
parts[#parts + 1] = "claude"
parts[#parts + 1] = "-p"
if opts.model then
parts[#parts + 1] = "--model"
parts[#parts + 1] = shell_escape(opts.model)
end
if opts.max_tokens then
parts[#parts + 1] = "--max-tokens"
parts[#parts + 1] = tostring(opts.max_tokens)
end
if opts.cwd then
parts[#parts + 1] = "--cwd"
parts[#parts + 1] = shell_escape(opts.cwd)
end
parts[#parts + 1] = shell_escape(prompt)
parts[#parts + 1] = "2>/dev/null"
local cmd = table.concat(parts, " ")
local handle = io.popen(cmd, "r")
if not handle then
return { ok = false, content = nil, error = "failed to spawn claude CLI" }
end
local content = handle:read("*a")
local close_ok, _, close_code = handle:close()
local is_success
if close_ok == nil then
is_success = content ~= nil and #content > 0
else
is_success = close_ok == true
end
if is_success then
if content and content:sub(-1) == "\n" then
content = content:sub(1, -2)
end
return { ok = true, content = content, error = nil }
else
return {
ok = false,
content = nil,
error = string.format("claude CLI exited with code %s", tostring(close_code or "?")),
}
end
end
setmetatable(M, {
__call = function(_, opts)
opts = opts or {}
if opts.model ~= nil and type(opts.model) ~= "string" then
error(string.format("claude_cli: 'model' must be string, got %s", type(opts.model)), 2)
end
if opts.system ~= nil and type(opts.system) ~= "string" then
error(string.format("claude_cli: 'system' must be string, got %s", type(opts.system)), 2)
end
if opts.max_tokens ~= nil and type(opts.max_tokens) ~= "number" then
error(string.format("claude_cli: 'max_tokens' must be number, got %s", type(opts.max_tokens)), 2)
end
if opts.cwd ~= nil and type(opts.cwd) ~= "string" then
error(string.format("claude_cli: 'cwd' must be string, got %s", type(opts.cwd)), 2)
end
return function(input)
local prompt = input
if opts.system then
prompt = string.format(
"<system>\n%s\n</system>\n\n%s",
opts.system, input
)
end
local start = std.time()
local result = cli_call(prompt, {
model = opts.model,
max_tokens = opts.max_tokens,
cwd = opts.cwd,
})
local elapsed = (std.time() - start) * 1000
if result.ok then
return {
text = result.content or "",
latency_ms = elapsed,
model = opts.model or "claude",
raw = result,
}
else
return {
text = "",
latency_ms = elapsed,
model = opts.model or "claude",
error = result.error,
raw = result,
}
end
end
end,
})
return M