local M = {}
function M.client(url, opts)
opts = opts or {}
if not url then
url = env.get("OPENCLAW_URL") or env.get("CLAWD_URL")
if not url then
error("openclaw: url required (set $OPENCLAW_URL or $CLAWD_URL)")
end
end
local base_url = url:gsub("/+$", "")
local token = opts.token
if not token then
token = env.get("OPENCLAW_TOKEN") or env.get("CLAWD_TOKEN")
end
local state_dir = opts.state_dir
if not state_dir then
state_dir = env.get("ASSAY_STATE_DIR") or env.get("OPENCLAW_STATE_DIR")
if not state_dir then
local home = env.get("HOME") or "/tmp"
state_dir = home .. "/.assay/state"
end
end
local function headers()
local h = { ["Content-Type"] = "application/json" }
if token then
h["Authorization"] = "Bearer " .. token
end
return h
end
local function api_post(path_str, payload)
local resp = http.post(base_url .. path_str, payload, { headers = headers() })
if resp.status ~= 200 and resp.status ~= 201 then
error("openclaw: POST " .. path_str .. " HTTP " .. resp.status .. ": " .. resp.body)
end
return json.parse(resp.body)
end
local function invoke(tool, action, args)
return api_post("/tools/invoke", {
tool = tool,
action = action,
args = args or {},
})
end
local c = {}
c.tools = {}
function c.tools:invoke(tool, action, args)
return invoke(tool, action, args)
end
c.messaging = {}
function c.messaging:send(channel, target, message)
return invoke("message", "send", {
channel = channel,
target = target,
message = message,
})
end
function c.messaging:notify(target, message)
return invoke("message", "send", {
target = target,
message = message,
})
end
c.cron = {}
function c.cron:add(job)
return invoke("cron", "add", { job = job })
end
function c.cron:list()
return invoke("cron", "list", {})
end
c.sessions = {}
function c.sessions:spawn(task, spawn_opts)
spawn_opts = spawn_opts or {}
local args = { task = task }
if spawn_opts.model then args.model = spawn_opts.model end
if spawn_opts.timeout then args.timeout = spawn_opts.timeout end
return invoke("sessions_spawn", "invoke", args)
end
c.state = {}
function c.state:get(key)
local path = state_dir .. "/" .. key .. ".json"
if not fs.exists(path) then return nil end
local content = fs.read(path)
if not content or content == "" then return nil end
return json.parse(content)
end
function c.state:set(key, value)
local path = state_dir .. "/" .. key .. ".json"
local dir = state_dir
if not fs.exists(dir) then
fs.mkdir(dir)
end
fs.write(path, json.encode(value))
return true
end
function c.state:diff(key, new_value)
local before = c.state:get(key)
c.state:set(key, new_value)
local changed = json.encode(before) ~= json.encode(new_value)
return {
changed = changed,
before = before,
after = new_value,
}
end
c.gates = {}
function c.gates:approve(prompt, context)
local approval_result = env.get("ASSAY_APPROVAL_RESULT")
if approval_result then
return approval_result == "yes"
end
local mode = env.get("ASSAY_MODE")
if mode == "tool" then
error("__assay_approval_request__:" .. json.encode({
prompt = prompt,
context = context,
}))
end
local interactive = env.get("OPENCLAW_INTERACTIVE") or env.get("TTY")
if interactive then
log.info("Approval required: " .. prompt)
if context then
log.info("Context: " .. json.encode(context))
end
return false
end
error("openclaw: approval_required: " .. json.encode({
type = "approval_request",
prompt = prompt,
context = context,
}))
end
c.llm = {}
function c.llm:task(prompt, llm_opts)
llm_opts = llm_opts or {}
local args = { prompt = prompt }
if llm_opts.model then args.model = llm_opts.model end
if llm_opts.artifacts then args.artifacts = llm_opts.artifacts end
if llm_opts.output_schema then args.output_schema = llm_opts.output_schema end
if llm_opts.temperature then args.temperature = llm_opts.temperature end
if llm_opts.max_output_tokens then args.max_output_tokens = llm_opts.max_output_tokens end
return invoke("llm-task", "invoke", args)
end
return c
end
return M