Lua API
Lua plugins are experimental and off by default. Turn them on with lua_plugins = true under [experimental] in the global caudra.toml, then restart Caudra. --no-plugins turns Lua off again for one run. See Experimental features.
Caudra plugins are plain Lua files. Everything a plugin can touch lives under
one global table: caudra. This reference documents every module, function,
and method. It is generated straight from the source code by caudra-docgen.
For where plugin files live and how to load them, read the
Plugins guide first.
The API tries to mirror Neovim as much as possible (caudra.fs, caudra.uv,
caudra.treesitter, caudra.keymap, caudra.base64), signatures are kept identical
so code can be copy-pasted between the two without too many modifications.
Plugins run compiled to native code (Luau JIT). If you are debugging a
plugin and want full backtraces, start caudra with --no-jit: it runs your
Lua on the interpreter with complete debug info instead.
A small plugin looks like this:
caudra.api.register_command({ name = "greet", description = "Say hello from Lua", handler = function() caudra.ui.flash("hello from a plugin!") end,})How to read this reference
Section titled “How to read this reference”Signatures use Neovim notation: {path} is a required argument, {opts?}
is optional, and {...} is variadic.
One convention to remember: fallible runtime operations return a
(value, err) pair instead of throwing. Check err before using value:
local text, err = caudra.fs.read("config.json")if err then caudra.log.error("read failed: " .. err) returnendLua errors are reserved for programmer mistakes, like passing a number where a string belongs.
Permissions and plugin.toml
Section titled “Permissions and plugin.toml”Sensitive APIs are gated per plugin file; every gated function's entry in
this reference names the permission it needs. The permissions are: fs_read, fs_write, net, run, env.
A gated call without its permission raises
permission denied: '<name>' not granted for this plugin.
Grants come from a plugin.toml next to the Lua file (for
~/.config/caudra/init.lua that is ~/.config/caudra/plugin.toml):
[permissions]fs_read = truefs_write = truenet = truerun = trueenv = trueThe rules:
- No
plugin.tomlat all: every permission is denied, and caudra logs a warning at load time. plugin.tomlexists: permissions default to granted; set a key tofalseto revoke it. An empty file grants everything.- Invalid TOML, or a top-level
versionnewer than this build reads: everything denied, with a warning in the log.versionis optional and a file without it counts as version 1.
Overview
Section titled “Overview”| Module | What it is for |
|---|---|
caudra | The global entry point. |
caudra.api | Plugin registration. |
caudra.agent | Subagent primitives for plugins that need to talk to an LLM. |
caudra.agent.Session | A subagent session with its own conversation history. |
caudra.async | Tools for running things concurrently in Lua plugins. |
caudra.async.Semaphore | A counting semaphore for limiting how many tasks run at once. |
caudra.async.Permit | One slot in a semaphore, obtained from Semaphore:acquire(). |
caudra.base64 | Base64 encoding and decoding, modelled after vim.base64. |
caudra.env | Paths to caudra's own directories (config, state, logs). |
caudra.fn | Process and environment helpers, modeled after Neovim's vim.fn job |
caudra.fs | File-system utilities, modelled after vim.fs and vim.uv. |
caudra.image | Small building blocks for working with images: probe metadata, decode |
caudra.image.Image | A decoded image you can inspect, resize, and re-encode. |
caudra.interpreter | Run Python code in a memory-safe, time-limited sandbox. |
caudra.json | JSON encoding, decoding, and schema validation. |
caudra.json.SchemaValidator | A compiled JSON Schema validator. |
caudra.keymap | Key mappings, modeled after vim.keymap. |
caudra.log | Structured logging for plugins. |
caudra.model | The model behind the focused session. |
caudra.net | HTTP client for fetching web content. |
caudra.session | Host session primitives. |
caudra.task | The subagents of the focused session and their transcripts. |
caudra.text | Text transformation utilities. |
caudra.treesitter | Tree-sitter parsing and query API. |
caudra.treesitter.language | Language registry for tree-sitter grammars. |
caudra.treesitter.query | Query compilation and lookup. |
caudra.treesitter.Query | A compiled tree-sitter query. |
caudra.treesitter.Tree | A parsed syntax tree. |
caudra.treesitter.Node | A single node in a parsed syntax tree. |
caudra.treesitter.LanguageTree | Manages parsing of a source string for a single language. |
caudra.ui | Functions for building interactive UI. |
caudra.ui.Win | Handle to a floating or split window. |
caudra.ui.Buf | A content buffer that holds styled lines of text. |
caudra.uv | System and environment utilities, modelled after vim.uv. |
caudra.yaml | YAML encoding and decoding. |
caudra
Section titled “caudra”The global entry point. Every API lives under this table.
caudra.setup()
Section titled “caudra.setup()”caudra.setup({config})Apply your personal configuration. This is only available inside init.lua (not in plugins) and can be called at most once. The table accepts the same keys as the Configuration reference.
Parameters:
{config}(table) Configuration table.
Example:
caudra.setup({model = "opus",keymaps = false,})caudra.split()
Section titled “caudra.split()”caudra.split({s}, {sep}, {opts?})Split {s} at each occurrence of {sep} and return the pieces as a
list. Mirrors Neovim's vim.split, so code using it can be copied
between Neovim and caudra. {sep} is a Lua pattern unless plain is
set; an empty {sep} splits into single characters.
Parameters:
{s}(string) String to split.{sep}(string) Separator: a Lua pattern, or literal text withplain.{opts?}(table?) Optional settings:plain(boolean?) treat {sep} as literal text instead of a pattern.trimempty(boolean?) drop empty pieces from the start and end of the result.
Returns: (table) List of split pieces.
Example:
caudra.split("a,b,c", ",") -- { "a", "b", "c" }caudra.split("x*y*z", "*", { plain = true }) -- { "x", "y", "z" }caudra.split("\nhello\nworld\n", "\n", { trimempty = true }) -- { "hello", "world" }caudra.version()
Section titled “caudra.version()”caudra.version()Return the version of the running Caudra build. Mirrors Neovim's
vim.version(), so one init.lua can adapt to several releases instead
of declaring a config version.
Returns: (table) major, minor and patch integers, plus a prerelease
string on pre-release builds.
Example:
local v = caudra.version()if v.major > 0 or v.minor >= 2 then -- use a setting added in 0.2endcaudra.api
Section titled “caudra.api”Plugin registration. This is where you tell caudra about your tools, slash commands, and prompt contributions.
Most plugins only need register_tool and maybe register_prompt_hint.
Call these at the top level of your plugin file (during load).
caudra.api.register_tool({ name = "greet", ... })caudra.api.register_prompt_hint({ slot = "tool_usage", content = "..." })caudra.api.register_tool()
Section titled “caudra.api.register_tool()”caudra.api.register_tool({spec})Register a new tool the agent can call. This is the main way plugins add capabilities to the agent. The tool is queued during plugin load and committed to the registry once the plugin finishes loading.
Your {spec} table must include a name, a description (the model reads it
to decide when to use the tool), a JSON Schema for the input, and a handler
function. The handler receives (input, ctx) and returns either a plain
string or a table with richer output fields. Normally return the complete
llm_output: the host applies output limits and retains eligible full text
for later retrieval. Truncate in the producer only when loss is intentional.
Parameters:
{spec}(table) Tool specification:name(string) Required. ASCII identifier, up to 64 chars ([a-zA-Z_][a-zA-Z0-9_]*).description(string) Required. Non-empty description shown to the model.schema(table) Required. JSON Schema object describing the tool's input parameters.handler(function) Required. Called with(input, ctx)when the tool is invoked. Must return a string or a table with any of these fields:llm_output(string) Text sent to the model.is_error(boolean) When true, the result is treated as an error.content(string) Alias for llm_output (legacy).body(BufHandle) Rich rendered body shown in the UI.header(BufHandle) One-line header shown before the body.format(string) "plain" (default) or "markdown".annotation(string) Short label shown next to the tool call.written_path(string) Path of a file written by the tool.diff_path(string) Path for a diff output block.diff_before(string) Before text of the diff.diff_after(string) After text of the diff.image(table) { media_type: string, data: string } base64 image.instructions(table) Array of { path, content } blocks injected as context.state(any) Serializable state forwarded to restore.model_suffix(string) Extra parent-model context omitted from UI and direct tool calls.output_limits(table) Host-enforced limits for this result.max_linesandmax_bytesare required positive integers. Overrides the agent defaults.
audiences(string[]) Which model audiences see the tool. Values: "main", "sub", "all". Default: all audiences.effect(string) Bundled-tool capability: "read_only", "isolated", "orchestrator", or "mutating". Unbundled claims are ignored.kind(string) Optional grouping label (e.g. "filesystem").timeout(number) Execution timeout in seconds. 0 or false disables. Default: inherits agent deadline.header(function) Optional. Called before execution, returns a string or BufHandle for the one-line header.restore(function) Optional. Called to re-render a previous tool result. Receives(tool_name, input, output, ctx).start(function) Optional. Called when the tool call starts, before the handler runs.describe(function) Optional. Returns a custom description string for the current context.examples(table) Optional. Array of example input objects for documentation.permission_scopes(string|function) Field name in schema (string) orfunction(input)returning a list of path scopes that need write permission.mutable_path(string) Schema field name (type: string) for the primary path the tool writes.start_annotation(string|table) Schema field used to annotate the start header with a count (string) or timeout ({ field, kind="timeout" }).
Example:
caudra.api.register_tool({ name = "word_count", description = "Count words in a file.", kind = "read", schema = { properties = { path = { type = "string", description = "File path" } }, required = { "path" }, }, handler = function(input) local f = io.open(input.path, "r") if not f then return { llm_output = "file not found", is_error = true } end local n = 0 for _ in f:read("*a"):gmatch("%S+") do n = n + 1 end f:close() return tostring(n) .. " words" end,})caudra.api.register_permission_rule()
Section titled “caudra.api.register_permission_rule()”caudra.api.register_permission_rule({spec})Declare an agent permission rule for a native tool. Use it to pre-allow (or pre-deny) tool calls on paths your plugin owns, like a storage directory outside the working dir, so the user is not prompted for them.
Rules live as long as the plugin is loaded: a reload replaces them, and a reload that registers none clears the old ones. User config and session deny rules always win over a plugin allow.
Parameters:
{spec}(table) Rule specification:tool(string) Required. Native tool name (e.g. "edit", "write"). MCP tools and the "*" wildcard are not allowed.scope(string) Required. Scope pattern the rule applies to, e.g. "/abs/dir/**" for a directory subtree.effect(string) Optional. "allow" (default) or "deny".
Example:
caudra.api.register_permission_rule({ tool = "write", scope = notes_dir .. "/**",})caudra.api.register_command()
Section titled “caudra.api.register_command()”caudra.api.register_command({spec})Register a slash-command that appears in the user input bar.
Slash commands let the user trigger plugin actions by typing /name in the
input. Use them for interactive workflows that do not need the model, like
browsing memory files or toggling settings.
Parameters:
{spec}(table) Command specification:name(string) Required. The command name (e.g. "/hello"; a leading slash is added when missing).description(string) Optional. Short description shown in the command palette.nargs(integer|string) Optional. How many arguments the command takes, spelled like nvim's nargs: 0 (default), 1, "?" (zero or one), "*" (any number), or "+" (one or more). An argument is a whitespace separated word. Type more than allowed and the command quietly stops matching: the input goes to the model as a normal message. Only the upper bound is checked, so with "+" you still need to handle an emptyopts.argsyourself.handler(function) Required. Called when the user runs the command, with one opts table:opts.argsis the raw argument string (whitespace kept, may be empty) andopts.fargsis the same split into words.
Example:
caudra.api.register_command({ name = "/hello", description = "Say hello", handler = function() caudra.ui.flash("Hello from my plugin!") end,})caudra.api.register_prompt_hint()
Section titled “caudra.api.register_prompt_hint()”caudra.api.register_prompt_hint({spec})Add a piece of text to an aggregate prompt slot. Multiple plugins can each contribute to the same slot, and all contributions are concatenated.
Good for things like tool usage guidelines or extra context that should
appear alongside other plugins' hints. If you need to own the whole slot
(e.g. identity or tone), use set_prompt instead.
Throws if you pass a singleton slot name.
Parameters:
{spec}(table) Hint specification:slot(string) Required. Aggregate slot name (e.g. "tool_usage", "general").content(string|function) Required. Static text, or afunction(config)that receives the effective agent config and returns a string or nil. Max 1 MiB.prompt(string|string[]) Optional. Restrict to specific prompt ids (e.g. "system").
Example:
caudra.api.register_prompt_hint({ slot = "tool_usage", content = "- Prefer **grep** over reading entire files.",})caudra.api.register_options()
Section titled “caudra.api.register_options()”caudra.api.register_options({spec})Declare the options your plugin accepts under plugins.<name> in
caudra.setup, and get back what the user set merged with your defaults.
Call it once, at the top level of your plugin file.
An unknown key, a wrong type, or a value below min fails the plugin
load with a clear message, so users catch typos right away. Bad specs
fail the load too. The specs also feed the generated configuration docs.
Parameters:
{spec}(table) Map of option name to a spec table:default(boolean|number|string) Optional. Used when the user sets nothing. Its Lua type becomes the option type.type(string) Required when there is no default: "boolean", "integer", "number", or "string".min(number) Optional. Minimum accepted value, numeric options only.desc(string) Required. One line shown in the configuration docs.
Returns: (table) Merged options: the user's value where set, otherwise the default, or nil when neither exists.
Example:
local opts = caudra.api.register_options({ timeout_secs = { default = 120, min = 5, desc = "Kill the command after this many seconds." }, max_output_lines = { type = "integer", desc = "Override agent.max_output_lines for this tool." },})caudra.api.set_prompt()
Section titled “caudra.api.set_prompt()”caudra.api.set_prompt({spec})Set a singleton prompt slot. Only one plugin owns each singleton slot at a time, so calling this replaces any previous value from your plugin.
Use this for slots like "identity" or "tone" where a single coherent value
makes more sense than combining fragments. For aggregate slots like
"tool_usage", use register_prompt_hint instead.
Throws if you pass an aggregate slot name.
Parameters:
{spec}(table) Spec fields mirrorregister_prompt_hint:slot(string) Required. Singleton slot name (e.g. "identity", "tone").content(string|function) Required. Static text, or afunction(config)that receives the effective agent config and returns a string or nil. Max 1 MiB.prompt(string|string[]) Optional. Restrict to specific prompt ids.
Example:
caudra.api.set_prompt({ slot = "tone", content = "Be concise. No filler words.",})caudra.api.get_tools()
Section titled “caudra.api.get_tools()”caudra.api.get_tools({opts?})Return a list of all registered tools. Useful for building UI that shows available tools or for checking which tools are enabled.
Each entry has the tool's name, schema, audiences, and an enabled flag.
Describe callbacks are not invoked (the static description is used).
Parameters:
{opts?}(table?) Options:config(table) Optional config table with adisabled_toolsstring[] field used to compute theenabledflag on each entry.
Returns: (table[]) Array of tool entries: { name, schema, audiences, kind?, enabled }.
Example:
local tools = caudra.api.get_tools()for _, t in ipairs(tools) do print(t.name, t.enabled)endcaudra.api.get_tool()
Section titled “caudra.api.get_tool()”caudra.api.get_tool({name})Look up a single tool by name. Returns its metadata table or nil if the
tool does not exist. For Lua-registered tools the returned table also
includes header and restore handle functions (wrapped so they never
throw).
Parameters:
{name}(string) Exact tool name.
Returns: (table|nil) Tool entry with fields { name, schema, audiences, kind?, header?, restore? }, or nil if not found.
Example:
local t = caudra.api.get_tool("bash")if t then print("bash audiences:", table.concat(t.audiences, ", "))endcaudra.api.tool_header()
Section titled “caudra.api.tool_header()”caudra.api.tool_header({name}, {input})The one-line summary a tool shows for a given call, as the transcript
would render it. Native tools have no Lua header, so a plugin that
presents someone else's call needs this to avoid falling back to the bare
tool name.
Parameters:
{name}(string) Exact tool name.{input}(table) Arguments the tool was called with.
Returns: string Header text, or the tool name when it cannot be summarized.
Example:
local text = caudra.api.tool_header("file_read", { path = "src/main.rs" })caudra.api.run_command()
Section titled “caudra.api.run_command()”caudra.api.run_command({cmdline})Runs a slash command by name, exactly as typing it in the input would.
Works for built-ins, custom /project: and /user: commands, MCP
prompts, and commands other plugins registered.
Use it to alias a command you like under a name you prefer, instead of
reimplementing what it does. See caudra.ui.action for the same idea
applied to keybound UI actions.
Pass the whole command line, arguments included: "/cd ~/src". The
leading slash is optional. Names match exactly apart from case, so a typo
reports an error instead of running the closest command, and a cycle of
aliases stops with one too.
This returns as soon as the command has been dispatched, not when it
finishes, so aliasing something long-running like /compact does not
block your handler.
Parameters:
{cmdline}(string) Command line, e.g."/new"or"/cd ~/src".
Returns: (boolean|nil, string|nil) true once dispatched, or nil and an error message for an unknown command.
Example:
-- /resume as an alias for the built-in session picker:caudra.api.register_command({ name = "/resume", description = "Alias for /sessions", handler = function() local ok, err = caudra.api.run_command("/sessions") if not ok then caudra.ui.flash("could not run /sessions: " .. err) end end,})caudra.api.create_autocmd()
Section titled “caudra.api.create_autocmd()”caudra.api.create_autocmd({event}, {opts})Listen for one or more events. Returns an id you can pass to
del_autocmd later to remove the listener.
Built-in events fired by the host: "TurnStart", "TurnEnd",
"TurnError", "ToolStart", "ToolDone", "SessionReset",
"SessionFocusChanged", "SessionStatusChanged", and
"TaskStatusChanged". Plugins can also fire their own events with
exec_autocmds.
Each host event carries data.session_id. For "SessionReset" that
is the session being left behind; the other events name the session now
running or focused. Tool events also carry data.tool_id and data.tool.
"SessionFocusChanged" also carries data.previous_session_id except on
initial startup. "SessionStatusChanged" fires whenever a session moves
between "working", "needs_input", and "idle"; it carries
data.status, data.title, and data.focused (boolean).
"TaskStatusChanged" fires when a subagent starts, or when one moves
between "working", "done", and "error"; it carries data.id and
data.name alongside data.status. A task that comes back from disk
already finished stays quiet, so reloading a session does not replay
old tasks.
Parameters:
{event}(string|string[]) Event name or list of names.{opts}(table) Options:callback(function) called with an ev table{ id, event, match, data }.once(boolean) remove the handler after it fires once (default false).pattern(string|string[]) only fire when the pattern matches."*"matches everything. Omit to match all.
Returns: (integer) Autocmd id.
Example:
local id = caudra.api.create_autocmd("TurnEnd", { callback = function(ev) print("turn ended: " .. ev.event) end,})caudra.api.del_autocmd()
Section titled “caudra.api.del_autocmd()”caudra.api.del_autocmd({id})Remove a previously registered autocmd. Does nothing if the {id} does not exist.
Parameters:
{id}(integer) Id returned bycreate_autocmd.
Example:
caudra.api.del_autocmd(id)caudra.api.exec_autocmds()
Section titled “caudra.api.exec_autocmds()”caudra.api.exec_autocmds({event}, {opts?})Fire one or more events manually. Every matching autocmd callback runs synchronously before this function returns.
Parameters:
{event}(string|string[]) Event name or list of names to fire.{opts?}(table?) Options:pattern(string) passed to callbacks asev.match.data(any) arbitrary value passed asev.data.
Example:
caudra.api.exec_autocmds("MyEvent", { pattern = "init", data = { msg = "hello" },})caudra.api.declare_slot()
Section titled “caudra.api.declare_slot()”caudra.api.declare_slot({name}, {default})Create a named extension point owned by your plugin. You provide a
{default} function, and other plugins can wrap it with layers using
set_slot. The returned callable runs the full chain: outermost
layer first, then inward, ending at {default}.
Throws if another plugin already owns a slot with the same {name}.
Parameters:
{name}(string) Unique slot name, e.g."myplugin.render".{default}(function) Default implementation, called when no layers wrap it.
Returns: (function) Callable that dispatches through all layers.
Example:
local render = caudra.api.declare_slot("myplugin.render", function(text) return text:upper()end)print(render("hello")) -- HELLOcaudra.api.set_slot()
Section titled “caudra.api.set_slot()”caudra.api.set_slot({name}, {wrapper})Add a layer around an existing (or future) slot. Layers wrap the
default from the outside in. Each layer receives prev as its
first argument. Call prev(...) to continue down the chain.
Calling prev more than once throws.
You can call this before the owner runs declare_slot. The layer
is queued and attached when the slot is declared.
Parameters:
{name}(string) Slot name to wrap.{wrapper}(function) Layer:function(prev, ...). Callprev(...)to continue.
Example:
caudra.api.set_slot("myplugin.render", function(prev, text) return prev("[" .. text .. "]")end)caudra.api.get_slots()
Section titled “caudra.api.get_slots()”caudra.api.get_slots()List all known slots and their current state. Useful for debugging which plugins own or wrap each slot.
Returns: (table) Map of slot name to { owner, declared, fillers }.
Example:
for name, info in pairs(caudra.api.get_slots()) do print(name, info.owner, info.declared)endcaudra.agent
Section titled “caudra.agent”Subagent primitives for plugins that need to talk to an LLM.
This module gives you the building blocks: resolve which model to use, build a system prompt, list available tools, call a tool directly, or open a full session with its own conversation history.
Policy like retries, validation, and concurrency lives in the calling plugin, not here.
local tools = caudra.agent.tools(ctx, { audience = "general_sub" })local sess = caudra.agent.session(ctx, { system = "You are a helpful assistant.", tools = tools,})local r = sess:prompt("Hello!")print(r.text)sess:close()caudra.agent.resolve_model()
Section titled “caudra.agent.resolve_model()”caudra.agent.resolve_model({ctx}, {opts?})Look up the model that the current agent is using, or the one bound to
another purpose. Ask for "fast" when a subtask is simple (summaries,
classification) instead of hard-coding a model name.
The returned table has fields: id (string), provider (string),
spec (string).
Parameters:
{ctx}(LuaCtx) Agent context.{opts?}(table?) Optional fields:purpose(string?) which binding to resolve, one of"chat","plan","subagent","compact","title","goal","fast","best". Resolves to the model the user bound, or that purpose's default.spec(string?) exactprovider/modelspec, e.g."anthropic/claude-haiku-4-5". Takes precedence overpurpose.
Returns: (table?, string?) Model table on success, or (nil, err) on failure.
Example:
local model, err = caudra.agent.resolve_model(ctx, { purpose = "fast" })if err then error(err) endprint(model.spec)caudra.agent.system_prompt()
Section titled “caudra.agent.system_prompt()”caudra.agent.system_prompt({ctx}, {opts})Build a system prompt from a built-in template. Environment variables like
{cwd} are substituted automatically. Use this when you need a ready-made
prompt for a subagent session.
Parameters:
-
{ctx}(LuaCtx) Agent context. -
{opts}(table) Required fields:prompt_id(string) one of"research","general","system".
Optional fields:
instructions(string|boolean?) extra text appended to the prompt.trueloads instructions from the project.caudra/instructionsfile.falseor nil omits them.
Returns: (string?, string?) The assembled prompt string, or (nil, err) on failure.
Example:
local prompt, err = caudra.agent.system_prompt(ctx, { prompt_id = "research", instructions = true,})if err then error(err) endcaudra.agent.tools()
Section titled “caudra.agent.tools()”caudra.agent.tools({ctx}, {opts})Get the list of tool definitions for a given audience. Pass the result
straight into caudra.agent.session() or use it to inspect what tools are
available.
Parameters:
-
{ctx}(LuaCtx) Agent context. -
{opts}(table) Required fields:audience(string) tool audience filter, e.g."general","subagent","general_sub".
Optional fields:
only(string[]?) include only these tool names.except(string[]?) exclude these tool names.spec(string?) evaluate capability exclusions against this model spec.
Returns: (table?, string?) Array of tool definition tables, or (nil, err) on failure.
Example:
local defs, err = caudra.agent.tools(ctx, { audience = "general_sub", except = { "bash", "write" },})if err then error(err) endprint(#defs .. " tools available")caudra.agent.call_tool()
Section titled “caudra.agent.call_tool()”caudra.agent.call_tool({ctx}, {name}, {input}, {opts?})Run a tool by name and wait for the result. This is how you call built-in
tools (like file_read, shell, file_glob) from Lua without going through the LLM.
Live events (streaming output, annotations, cumulative usage) are delivered through optional callbacks while the tool runs.
Parameters:
{ctx}(LuaCtx) Agent context.{name}(string) Tool name, e.g."bash","read".{input}(table|any) Tool input (JSON-serializable). Must match the tool'sinput_schema.{opts?}(table?) Optional fields:timeout(integer?) deadline in seconds.on_live_buf(function?) called with aBufHandlefor each live buffer the tool publishes. Must not yield.on_annotation(function?) called with an annotation string for each annotation event. Must not yield.on_usage(function?) called with a formatted cumulative token usage string. Must not yield.on_progress(function?) called with(label, detail, tally)whenever a dispatched subagent moves.labelis a tool name or one of"thinking","responding","compacting","retrying","awaiting permission";detailis the tool header, or nil;tallyreads like"3 tools · 12.4s". Must not yield.
Returns: (string?, string?, string?, boolean?, string?) Tool output text as
the model sees it, error, generated call ID, whether an error restore is
authorized, and the same result written for a reader. The last differs
for tools whose model output is a structured record: show it instead of
the first when presenting the call to a person.
Example:
local out, err = caudra.agent.call_tool(ctx, "bash", { command = "ls -la", timeout = 10,})if err then error(err) endprint(out)caudra.agent.session()
Section titled “caudra.agent.session()”caudra.agent.session({ctx}, {opts})Create a new subagent session. The session uses the global Subagent model,
which inherits the parent model when unbound, and inherits the MCP handle.
You can override either. You get back a Session object that you can send
messages to with :prompt().
This is the main way to spin up a sub-conversation with its own history and tool set.
Parameters:
-
{ctx}(LuaCtx) Agent context. -
{opts}(table) Optional fields:model_spec(string?) exact model spec to use instead of the Subagent binding.system(string?) system prompt. Defaults to empty.tools(table?) tool definitions array (fromcaudra.agent.tools()).local_tools(table?) map ofname -> specfor Lua-backed tools. Each spec requiresdescription(string),input_schema(table), andhandler(function). Optionaleffectisread_only,isolated,orchestrator, ormutating. The handler receives the input table and must return(string)or(nil, err).name(string?) display name for logs and UI.task_id(string?) completed task to continue with its existing history.audience(string?) tool audience for capability gating. Default:"general_sub".mcp(boolean?) give the session access to MCP tools. Their definitions are injected automatically each turn (deferred behindtool_search), so don't put MCP definitions intools. The session starts with no loaded tools of its own. Default:true.thinking(string|integer?) thinking mode:"off","adaptive", an effort level ("minimal","low","medium","high","xhigh","max"), or a budget integer (token count). Inherits parent setting if omitted.fast(boolean?) use fast mode. Inherits parent setting if omitted.task(boolean?) enable the host-owned task path. Default:false.profile(string?) task system prompt profile. Requirestask = true.mode(string?) task mode:planorbuild. Requirestask = true.
Task sessions derive their model from the Subagent binding or an explicit
profile selector. Thinking may also come from the profile; system prompt,
tools, audience, and MCP access follow profile and mode. Do not combine
task = truewith the corresponding generic session options.
Returns: (Session?, string?) Session handle, or (nil, err) on failure.
Example:
local tools = caudra.agent.tools(ctx, { audience = "general_sub" })local sess, err = caudra.agent.session(ctx, { system = "You are a research assistant.", tools = tools, name = "researcher",})if err then error(err) endlocal result = sess:prompt("Summarize this file.")sess:close()caudra.agent.Session
Section titled “caudra.agent.Session”A subagent session with its own conversation history.
Create one with caudra.agent.session(), then send messages with
:prompt(). The session remembers previous turns, so you can have
a multi-step conversation. Call :close() when you are done, or let
garbage collection handle it.
Session:id()
Section titled “Session:id()”Session:id()Return the stable task ID used for continuation and UI routing.
Returns: string
Session:prompt()
Section titled “Session:prompt()”Session:prompt({message})Send a message to the subagent and wait for its full response. The agent loop runs to completion, calling tools as needed. Conversation history is kept across calls, so you can have a multi-turn conversation.
The returned table has fields: text (string), duration_ms (integer),
input_tokens (integer), output_tokens (integer). text is an empty
string when the subagent produced no text block (e.g. it only called
tools).
Parameters:
{message}(string) User message to send.
Returns: (table?, string?) Result table on success, or (nil, err) on
failure. A run cut short after streaming some text hands you both: the
error and a { text = <what it streamed> } table.
Example:
local r, err = sess:prompt("What files are in this project?")if err then error(err) endprint(r.text)print(r.input_tokens .. " input, " .. r.output_tokens .. " output tokens")Session:close()
Section titled “Session:close()”Session:close()Close the session and flush its history back to the parent agent. You can call this multiple times safely. If you forget, it runs automatically when the session is garbage collected.
caudra.async
Section titled “caudra.async”Tools for running things concurrently in Lua plugins.
Use run to fire off background tasks, gather or join to run
several functions at once, and semaphore to limit concurrency.
The await and wrap helpers bridge callback-based APIs into
coroutine-friendly calls.
local results = caudra.async.gather({ function() return fetch("a.txt") end, function() return fetch("b.txt") end,})caudra.async.run()
Section titled “caudra.async.run()”caudra.async.run({fn}, {on_finish?})Fire off a function as a new async task. It runs in the background and you do not wait for it. If you need the result, pass an {on_finish} callback.
Parameters:
{fn}(function) Zero-argument function to execute.{on_finish?}(function?) Optional callbackfunction(err, result). Called once {fn} completes.
Example:
caudra.async.run(function() local data = expensive_fetch() process(data)end)caudra.async.await()
Section titled “caudra.async.await()”caudra.async.await({argc}, {fn}, {...})Turn a callback-based function into a normal call you can use in a coroutine. It calls fn(..., callback), inserting the callback at position {argc}, then suspends your coroutine until the callback fires. You get back whatever the callback was called with.
Parameters:
{argc}(integer) Total number of positional arguments {fn} expects (including the callback). Must be >= 1.{fn}(function) Callback-based function to call.{...}(any) Extra arguments forwarded to {fn} before the injected callback.
Returns: (...) Values passed by the caller to the injected callback.
Example:
local result = caudra.async.await(2, http.get, url)caudra.async.wrap()
Section titled “caudra.async.wrap()”caudra.async.wrap({argc}, {fn})Create a coroutine-friendly wrapper around a callback-based function. The wrapper calls caudra.async.await for you, so you can use the result like a normal function call.
Parameters:
{argc}(integer) Callback position, forwarded tocaudra.async.await.{fn}(function) Callback-based function to wrap.
Returns: (function) Wrapped function you can call like a normal function.
Example:
local get = caudra.async.wrap(2, http.get)local body = get(url)caudra.async.join()
Section titled “caudra.async.join()”caudra.async.join({max_jobs}, {fns})Run all functions in {fns} with at most {max_jobs} going at once. Waits until every function has finished. Unlike gather, this does not return individual results.
Parameters:
{max_jobs}(integer) Maximum number of functions running at the same time.{fns}(table) Array of zero-argument functions to execute.
Example:
caudra.async.join(4, { function() process(files[1]) end, function() process(files[2]) end, function() process(files[3]) end,})caudra.async.gather()
Section titled “caudra.async.gather()”caudra.async.gather({fns})Run all functions in {fns} at the same time and collect their results.
Unlike join, this gives you back the return value (or error) from each
function. The results are in the same order as the input.
Each entry in the result array has ok (boolean), and either value
(on success) or err (string, on failure).
Parameters:
{fns}(table) Array of zero-argument functions.
Returns: (table) Array of result tables, one per function.
Example:
local results = caudra.async.gather({ function() return fetch("a.txt") end, function() return fetch("b.txt") end,})for i, r in ipairs(results) do if r.ok then print(r.value) else print("error: " .. r.err) endendcaudra.async.semaphore()
Section titled “caudra.async.semaphore()”caudra.async.semaphore({n})Create a counting semaphore that allows at most {n} concurrent permits. Use this to limit how many tasks hit a resource at the same time.
Parameters:
{n}(integer) Maximum number of concurrent permits. Values below 1 are clamped to 1.
Returns: (caudra.async.Semaphore) A new semaphore.
Example:
local sem = caudra.async.semaphore(5)-- each task acquires a permit before doing worklocal permit = sem:acquire()do_work()permit:release()caudra.async.on_cancel()
Section titled “caudra.async.on_cancel()”caudra.async.on_cancel({fn})Register {fn} to run as soon as the current task is cancelled or hits
its deadline, without waiting for whatever it is doing to finish. Use
it to paint the cancelled state: a handler waiting on children
(gather, call_tool) stays parked until they wind down, so anything
after the wait is too late to reach the screen.
The callback receives the reason ("cancelled" or "timeout") and may
still call ctx:finish; the host prefers that reply over the generic
cancelled/timeout error. Mark it is_error = true and end it with a
marker, so the model knows the output it gets is cut short.
The callback runs outside your coroutine, so it must not yield. It fires at most once, immediately if the task is already cancelled. An error inside it is logged and never reaches your handler, and the other hooks still run.
Parameters:
{fn}(function) Function to run on cancel; receives the reason string.
Example:
caudra.async.on_cancel(function(reason) view:append({ { reason, "tool_error" } }) ctx:finish({ llm_output = partial .. "\n[cancelled; output is partial]", is_error = true })end)caudra.async.gather(children)caudra.async.Semaphore
Section titled “caudra.async.Semaphore”A counting semaphore for limiting how many tasks run at once.
Create one with caudra.async.semaphore(n), then call :acquire() to
get a permit before doing work. If the task is cancelled, the acquire
is cancelled too.
Semaphore:acquire()
Section titled “Semaphore:acquire()”Semaphore:acquire()Wait for a permit from the semaphore. Your coroutine suspends until a slot opens up. If the owning task is cancelled, the acquire is cancelled too.
Returns: (caudra.async.Permit) A permit handle. Call :release() when done, or let it be garbage collected.
Example:
local sem = caudra.async.semaphore(3)local permit = sem:acquire()-- do work that needs the slotpermit:release()caudra.async.Permit
Section titled “caudra.async.Permit”One slot in a semaphore, obtained from Semaphore:acquire().
The slot is held until you call :release() or until the permit is
garbage collected. Releasing early lets other tasks acquire sooner.
Permit:release()
Section titled “Permit:release()”Permit:release()Give the permit back to the semaphore so another task can acquire it. Throws if you already released this permit.
caudra.base64
Section titled “caudra.base64”Base64 encoding and decoding, modelled after vim.base64.
Both functions accept strings and Luau buffers, so you can round-trip
binary data read with caudra.fs.read_bytes.
local encoded = caudra.base64.encode("hello")local decoded = caudra.base64.decode(encoded)caudra.base64.encode()
Section titled “caudra.base64.encode()”caudra.base64.encode({data})Encode {data} to standard Base64. Like vim.base64.encode.
Accepts both strings and Luau buffers.
Parameters:
{data}(string|buffer) Data to encode.
Returns: (string) Base64-encoded string.
Example:
caudra.base64.encode("hello") -- "aGVsbG8="caudra.base64.decode()
Section titled “caudra.base64.decode()”caudra.base64.decode({str})Decode a Base64-encoded {str} back to its original bytes. Like vim.base64.decode.
Throws if {str} is not valid Base64.
Parameters:
{str}(string|buffer) Base64-encoded text.
Returns: (string) Decoded bytes as a string.
Example:
caudra.base64.decode("aGVsbG8=") -- "hello"caudra.env
Section titled “caudra.env”Paths to caudra's own directories (config, state, logs).
Use these to locate config files or persistent state without hard-coding paths.
local cfg = caudra.env.config_dir()caudra.env.state_dir()
Section titled “caudra.env.state_dir()”caudra.env.state_dir()Return the directory where caudra stores runtime state (sessions, auth tokens, etc.).
Typically ~/.local/state/caudra, or ~/.local/state/caudra-debug in debug builds.
CAUDRA_NAMESPACE overrides the directory name.
Requires the env plugin permission.
Returns: (string?) State directory path, or nil if it cannot be determined.
Example:
local dir = caudra.env.state_dir()caudra.env.config_dir()
Section titled “caudra.env.config_dir()”caudra.env.config_dir()Return the directory where caudra looks for user configuration files.
Typically ~/.config/caudra, or ~/.config/caudra-debug in debug builds.
CAUDRA_NAMESPACE overrides the directory name.
Requires the env plugin permission.
Returns: (string?) Config directory path, or nil if it cannot be determined.
Example:
local dir = caudra.env.config_dir()caudra.env.logs_dir()
Section titled “caudra.env.logs_dir()”caudra.env.logs_dir()Return the directory where caudra writes its log files (caudra.log).
Typically ~/.local/logs/caudra, or ~/.local/logs/caudra-debug in debug builds.
CAUDRA_NAMESPACE overrides the directory name.
Requires the env plugin permission.
Returns: (string?) Logs directory path, or nil if it cannot be determined.
Example:
local dir = caudra.env.logs_dir()caudra.fn
Section titled “caudra.fn”Process and environment helpers, modeled after Neovim's vim.fn job
control. Use these to run shell commands, wait for output, and check
whether programs are installed.
local id = caudra.fn.jobstart("git status", { on_exit = function(code) print("done: " .. code) end,})caudra.fn.jobstart()
Section titled “caudra.fn.jobstart()”caudra.fn.jobstart({cmd}, {opts?})Run a shell command in the background. The command runs through
bash -c on Unix or cmd /C on Windows. You get back a job id
that you can pass to jobstop or jobwait to control the process.
Requires the run plugin permission.
Parameters:
{cmd}(string) Shell command to run.{opts?}(table?) Optional settings:cwd(string?) working directory (tilde is expanded).env(table?) extra environment variables,{ VAR = "value" }.on_stdout(function?) called with(job_id, line)for each stdout line.on_stderr(function?) called with(job_id, line)for each stderr line.on_error(function?) called with(job_id, message)on stream failures.on_exit(function?) called with(job_id, code)when the process finishes.raw_chunks(boolean?) deliver bounded chunks with exact newlines instead of line callbacks. Defaults to false.owner(string?) job lifetime."task"(default) ends the job with the current call."plugin"keeps it alive until the plugin unloads or reloads.
Returns: (integer) Job id.
Example:
local id = caudra.fn.jobstart("ls -la", { cwd = "~/projects", on_stdout = function(_, line) print(line) end, on_exit = function(_, code) print("exit: " .. code) end,})caudra.fn.jobstop()
Section titled “caudra.fn.jobstop()”caudra.fn.jobstop({job_id})Kill a running job immediately (SIGKILL on Unix). Safe to call on jobs that already exited or on unknown ids.
Requires the run plugin permission.
Parameters:
{job_id}(integer) Job id returned byjobstart.
Example:
caudra.fn.jobstop(id)caudra.fn.jobwait()
Section titled “caudra.fn.jobwait()”caudra.fn.jobwait({job_id}, {timeout_ms?})Wait for a job to finish and collect its output. Returns a result
table with stdout, stderr, and exit_code. Returns nil if the
job does not finish before the timeout. Collection is limited to 16 MiB
across stdout and stderr; larger output raises an explicit error.
While waiting, the job's on_stdout, on_stderr, and on_exit
callbacks fire as events arrive (like Neovim), so you can stream
output into a buffer while parked here.
Requires the run plugin permission.
Parameters:
{job_id}(integer) Job id returned byjobstart.{timeout_ms?}(integer?) Maximum wait in milliseconds (default 30000).
Returns: (table?) { stdout, stderr, exit_code }, or nil on timeout.
Example:
local id = caudra.fn.jobstart("echo hello")local result = caudra.fn.jobwait(id, 5000)if result then print(result.stdout)endcaudra.fn.executable()
Section titled “caudra.fn.executable()”caudra.fn.executable({name})Check whether {name} can be found on $PATH or is an absolute path
to a file. Returns 1 when found, 0 otherwise (matches Neovim's
vim.fn.executable).
Requires the env plugin permission.
Parameters:
{name}(string) Program name (e.g."git") or absolute path.
Returns: (integer) 1 if found, 0 otherwise.
Example:
if caudra.fn.executable("rg") == 1 then -- use ripgrependcaudra.fn.winsaveview()
Section titled “caudra.fn.winsaveview()”caudra.fn.winsaveview()Read the viewport of the focused chat transcript, like Neovim's
vim.fn.winsaveview(). The transcript is the only scrollable window
caudra has, so there is no window argument.
topline is the 1-based transcript line at the top of the viewport, so
the last visible one is math.min(topline + height - 1, line_count).
auto_scroll has no Vim counterpart: it is true while the transcript
follows streaming output.
Returns: (table|nil, string|nil) {topline, line_count, height, auto_scroll}, or nil and an error.
Example:
local view = caudra.fn.winsaveview()caudra.fn.winrestview({ topline = view.topline + 1 })caudra.fn.winrestview()
Section titled “caudra.fn.winrestview()”caudra.fn.winrestview({view})Scroll the focused chat transcript so that the topline field of
{view} becomes the top visible line, like Neovim's
vim.fn.winrestview(). Out of range values are clamped. Other keys are
ignored, so a table straight from winsaveview() round-trips.
Scrolling away from the bottom unpins the transcript; landing back at the bottom re-pins it so streaming output keeps following.
Parameters:
{view}(table) View to restore. Onlytopline(1-based) is read.
Returns: (boolean|nil, string|nil) true on success, or nil and an error.
Example:
caudra.fn.winrestview({ topline = 1 })caudra.fs
Section titled “caudra.fs”File-system utilities, modelled after vim.fs and vim.uv.
Fallible operations return (value, err) pairs and never throw.
Paths support ~/ expansion. Relative paths resolve from the current working directory.
local text, err = caudra.fs.read("init.lua")if err then return endcaudra.fs.read()
Section titled “caudra.fs.read()”caudra.fs.read({path})Read the entire file at {path} as a UTF-8 string.
If the file contains bytes that are not valid UTF-8, this function throws.
Use read_bytes for binary files.
Requires the fs_read plugin permission.
Parameters:
{path}(string) Absolute or relative file path.~/is expanded to the home directory.
Returns: (string?, string?) File contents, or nil plus an error message.
Example:
local text, err = caudra.fs.read("config.toml")if err then caudra.log.warn("could not read config: " .. err) returnendcaudra.fs.read_bytes()
Section titled “caudra.fs.read_bytes()”caudra.fs.read_bytes({path})Read the entire file at {path} as raw bytes, returned as a Luau buffer.
Useful for binary files or when you need to pass the data to caudra.base64.encode.
Requires the fs_read plugin permission.
Parameters:
{path}(string) Absolute or relative file path.~/is expanded to the home directory.
Returns: (buffer?, string?) File bytes as a Luau buffer, or nil plus an error message.
Example:
local buf, err = caudra.fs.read_bytes("image.png")if err then return endlocal encoded = caudra.base64.encode(buf)caudra.fs.metadata()
Section titled “caudra.fs.metadata()”caudra.fs.metadata({path})Get metadata for the file or directory at {path}.
Returns a table with size (integer), is_file (boolean), is_dir (boolean),
and mtime (number, fractional seconds since the Unix epoch; absent when the
filesystem does not report a modification time).
If {path} does not exist, returns nil with no error.
Requires the fs_read plugin permission.
Parameters:
{path}(string) Absolute or relative path.
Returns: (table?, string?) Metadata table, nil if missing, or nil plus an error message.
Example:
local meta = caudra.fs.metadata("src/main.rs")if meta and meta.is_file then print("size: " .. meta.size)endcaudra.fs.dirname()
Section titled “caudra.fs.dirname()”caudra.fs.dirname({path})Return the parent directory of {path}. Like vim.fs.dirname.
Parameters:
{path}(string) File path.
Returns: (string?) Parent directory, or nil if {path} has no parent.
Example:
caudra.fs.dirname("/home/user/init.lua") -- "/home/user"caudra.fs.basename()
Section titled “caudra.fs.basename()”caudra.fs.basename({path})Return the final component (the file name) of {path}. Like vim.fs.basename.
Parameters:
{path}(string) File path.
Returns: (string?) File name, or nil for paths like /.
Example:
caudra.fs.basename("/home/user/init.lua") -- "init.lua"caudra.fs.joinpath()
Section titled “caudra.fs.joinpath()”caudra.fs.joinpath({...})Join one or more path segments into a single path. Like vim.fs.joinpath.
Parameters:
{...}(string) One or more path segments to join.
Returns: (string) The joined path.
Example:
caudra.fs.joinpath("src", "api", "fs.rs") -- "src/api/fs.rs"caudra.fs.normalize()
Section titled “caudra.fs.normalize()”caudra.fs.normalize({path})Clean up . and .. segments and make {path} absolute. Like vim.fs.normalize.
This is purely string-based and does not touch the filesystem.
Parameters:
{path}(string) Path to normalize.~/is expanded.
Returns: (string) Normalized absolute path.
Example:
caudra.fs.normalize("src/../src/api") -- "/home/user/project/src/api"caudra.fs.abspath()
Section titled “caudra.fs.abspath()”caudra.fs.abspath({path})Make {path} absolute by prepending the current working directory when needed.
Unlike normalize, this does not resolve . or .. segments.
Parameters:
{path}(string) Relative or absolute path.~/is expanded.
Returns: (string) Absolute path.
Example:
caudra.fs.abspath("src/main.rs") -- "/home/user/project/src/main.rs"caudra.fs.parents()
Section titled “caudra.fs.parents()”caudra.fs.parents({path})Return all ancestor directories of {path}, from the immediate parent up to the root. Handy for walking up a directory tree.
Parameters:
{path}(string) File or directory path.
Returns: (string[]) Array of ancestor directory paths.
Example:
local dirs = caudra.fs.parents("/home/user/project/src")-- { "/home/user/project", "/home/user", "/home", "/" }caudra.fs.root()
Section titled “caudra.fs.root()”caudra.fs.root({source}, {marker})Walk upward from {source} looking for a directory that contains one of the
{marker} files or directories. Like vim.fs.root. Useful for finding the
project root.
Requires the fs_read plugin permission.
Parameters:
{source}(string) Starting file or directory path.{marker}(string|string[]) Marker filename(s) to look for, e.g.".git"or{"package.json", ".git"}.
Returns: (string?, string?) Root directory path, or nil when not found.
Example:
local root = caudra.fs.root("src/main.rs", { ".git", "Cargo.toml" })if root then print("project root: " .. root) endcaudra.fs.relpath()
Section titled “caudra.fs.relpath()”caudra.fs.relpath({base}, {target})Compute a relative path from {base} to {target}.
Parameters:
{base}(string) Base directory path.{target}(string) Target path.
Returns: (string) Relative path from {base} to {target}.
Example:
caudra.fs.relpath("/home/user", "/home/user/project/src") -- "project/src"caudra.fs.ext()
Section titled “caudra.fs.ext()”caudra.fs.ext({path})Return the file extension of {path}, without the leading dot.
Parameters:
{path}(string) File path.
Returns: (string?) Extension, or nil if the path has no extension.
Example:
caudra.fs.ext("main.rs") -- "rs"caudra.fs.ext("Makefile") -- nilcaudra.fs.dir()
Section titled “caudra.fs.dir()”caudra.fs.dir({path}, {opts?})List the contents of the directory at {path}.
Each entry is a two-element array {name, type} where type is one of
"file", "directory", "link", or "unknown". Follows symlinks.
Requires the fs_read plugin permission.
Parameters:
{path}(string) Directory path.{opts?}(table?)depth(integer, default 1): how many levels deep to recurse.
Returns: (table?, string?) Array of {name, type} entries, or nil plus an error message.
Example:
local entries, err = caudra.fs.dir("src", { depth = 2 })if err then return endfor _, e in ipairs(entries) do print(e[1], e[2]) -- "main.rs" "file"endcaudra.fs.write()
Section titled “caudra.fs.write()”caudra.fs.write({path}, {content})Write {content} to the file at {path}, creating it if it does not exist or overwriting it if it does.
Requires the fs_write plugin permission.
Parameters:
{path}(string) Destination file path.~/is expanded.{content}(string) Text to write.
Returns: (true?, string?) true on success, or nil plus an error message.
Example:
local ok, err = caudra.fs.write("out.txt", "hello world")if err then print("write failed: " .. err) endcaudra.fs.atomic_write()
Section titled “caudra.fs.atomic_write()”caudra.fs.atomic_write({path}, {content})Atomically replace {path} with {content}. The parent directory must exist. Readers observe either the old file or the complete new file. Existing file permissions are preserved. On Unix, new files use mode 0600.
Requires the fs_write plugin permission.
Parameters:
{path}(string) Destination file path.~/is expanded.{content}(string) Text to write.
Returns: (true?, string?) true on success, or nil plus an error message.
Example:
local ok, err = caudra.fs.atomic_write("state.json", encoded)if err then print("atomic write failed: " .. err) endcaudra.fs.rm()
Section titled “caudra.fs.rm()”caudra.fs.rm({path}, {opts?})Delete the file, symlink, or directory at {path}.
Pass recursive = true to remove a non-empty directory tree (like rm -r).
Unlike vim.fs.rm, this also removes an empty directory without recursive.
Symlinks are removed themselves, never followed.
Requires the fs_write plugin permission.
Parameters:
{path}(string) Path to the file or directory to remove.{opts?}(table?)recursive(boolean, default false): remove a directory and its contents recursively.force(boolean, default false): silently ignore a missing path.
Returns: (true?, string?) true on success, or nil plus an error message.
Example:
local ok, err = caudra.fs.rm("temp.txt")if err then print("rm failed: " .. err) endcaudra.fs.rm("stale_dir", { recursive = true, force = true })caudra.fs.mkdir()
Section titled “caudra.fs.mkdir()”caudra.fs.mkdir({path}, {opts?})Create the directory at {path}. Set parents = true to create
intermediate directories, like mkdir -p.
Requires the fs_write plugin permission.
Parameters:
{path}(string) Directory path to create.{opts?}(table?)parents(boolean, default false): create intermediate parent directories.
Returns: (true?, string?) true on success, or nil plus an error message.
Example:
caudra.fs.mkdir("a/b/c", { parents = true })caudra.fs.glob()
Section titled “caudra.fs.glob()”caudra.fs.glob({pattern}, {opts?})Find files matching one or more glob patterns.
Respects .gitignore by default. Pass sort = "mtime" to get the most
recently modified files first.
Requires the fs_read plugin permission.
Parameters:
{pattern}(string|string[]) Glob pattern or array of patterns.{opts?}(table?)path(string): search root.limit(integer): max results.gitignore(boolean, default true): respect .gitignore.sort(string):"mtime"sorts newest first.
Returns: (string[]?, string?) Array of absolute file paths, or nil plus an error message.
Example:
local files, err = caudra.fs.glob("**/*.lua", { path = "plugins", limit = 10 })if err then return endfor _, f in ipairs(files) do print(f) endcaudra.fs.grep()
Section titled “caudra.fs.grep()”caudra.fs.grep({pattern}, {opts?})Search file contents for a regex {pattern}. Returns structured matches grouped by file, similar to ripgrep output.
Each result entry has a path and a list of groups. Each group contains
lines, where every line has line_nr, text, and is_match.
Requires the fs_read plugin permission.
Parameters:
{pattern}(string) Regular expression to search for.{opts?}(table?)path(string): search root.include(string): file glob filter (e.g."*.rs").context_before/context_after(integer): context lines around matches.limit(integer): max match groups.max_line_bytes(integer): skip lines longer than this.
Returns: (table?, string?) Array of {path, groups} tables, or nil plus an error message.
Example:
local hits, err = caudra.fs.grep("TODO", { path = "src", include = "*.rs", limit = 5 })if err then return endfor _, file in ipairs(hits) do for _, g in ipairs(file.groups) do for _, line in ipairs(g.lines) do if line.is_match then print(file.path .. ":" .. line.line_nr) end end endendcaudra.image
Section titled “caudra.image”Small building blocks for working with images: probe metadata, decode pixels, resize, and encode back to bytes. Plugins compose these freely.
Decoding is guarded against pixel-bomb attacks (50 MP limit).
local img = caudra.image.decode(raw_bytes)local small = img:resize(1024, 768)local png = small:encode("png")caudra.image.probe()
Section titled “caudra.image.probe()”caudra.image.probe({data})Read image metadata (format, dimensions) from raw bytes without fully
decoding the pixels. Much faster than decode when you only need to
check the size or format.
Returns a table with format (string), width (integer), height
(integer), or (nil, err) if the bytes are not a recognized image.
Parameters:
{data}(string|buffer) Raw image bytes.
Returns: (table?, string?) Info table, or (nil, err) on failure.
Example:
local info, err = caudra.image.probe(raw_bytes)if err then error(err) endprint(info.format, info.width, info.height)caudra.image.decode()
Section titled “caudra.image.decode()”caudra.image.decode({data})Decode raw image bytes into an Image handle you can resize and re-encode. Images larger than 50 megapixels are rejected to prevent memory bombs.
Parameters:
{data}(string|buffer) Raw image bytes.
Returns: (caudra.image.Image?, string?) Decoded image, or (nil, err) on failure.
Example:
local img, err = caudra.image.decode(raw_bytes)if err then error(err) endprint(img:width() .. "x" .. img:height())caudra.image.Image
Section titled “caudra.image.Image”A decoded image you can inspect, resize, and re-encode.
Get one from caudra.image.decode(). The image data lives in memory
until the handle is garbage collected.
Image:width()
Section titled “Image:width()”Image:width()Get the width of the image in pixels.
Returns: (integer) Width in pixels.
Image:height()
Section titled “Image:height()”Image:height()Get the height of the image in pixels.
Returns: (integer) Height in pixels.
Image:resize()
Section titled “Image:resize()”Image:resize({max_w}, {max_h})Shrink the image to fit inside {max_w} x {max_h}, keeping the aspect ratio. If the image already fits, it is returned as-is. Never upscales.
Parameters:
{max_w}(integer) Maximum width in pixels. Must be positive.{max_h}(integer) Maximum height in pixels. Must be positive.
Returns: (caudra.image.Image) A new image handle (or the same one if no resize was needed).
Example:
local img = caudra.image.decode(raw_bytes)local small = img:resize(800, 600)local encoded = small:encode("jpeg")Image:encode()
Section titled “Image:encode()”Image:encode({format})Encode the image into raw bytes in the given format. Use this to prepare images for sending over the network or writing to disk.
Parameters:
{format}(string) Output format:"png","jpeg", or"jpg".
Returns: (string) Encoded image bytes.
Example:
local bytes = img:encode("png")-- bytes is a Lua string containing the raw PNG datacaudra.interpreter
Section titled “caudra.interpreter”Run Python code in a memory-safe, time-limited sandbox.
The sandbox uses the monty interpreter. Python code can call back into Lua-defined tools, and stdout is streamed line by line.
local r, err = caudra.interpreter.run("print('hello')", { timeout = 10, max_memory_mb = 128, on_output = function(line) print(line) end,})caudra.interpreter.run()
Section titled “caudra.interpreter.run()”caudra.interpreter.run({code}, {opts})Run Python code in a sandboxed interpreter with memory and time limits. Stdout lines are streamed to your {on_output} callback as they are produced. If the Python code calls tools, those calls are dispatched to the Lua functions you provide in {opts}.tools.
The result table has optional fields: stdout (string, trimmed combined
output) and output (string, the final expression value). On error, the
table is empty and the second return value is the error message.
Requires the run plugin permission.
Parameters:
-
{code}(string) Python source code to execute. -
{opts}(table) Required fields:timeout(integer) execution time limit in seconds.max_memory_mb(integer) memory limit in megabytes.on_output(function) called with each stdout line (string) as it is produced. Must not yield.
Optional fields:
preamble(string?) Python source (imports, helpers) compiled ahead of {code}. Tracebacks are rebased so line 1 is {code} line 1.tools(table?) map ofname -> functionfor tools the sandbox may call. Each function receives the tool input table and must return(string)or(nil, err). Tool calls are batched and dispatched concurrently.
Returns: (table, string?) Result table, plus an error string on failure.
Example:
local result, err = caudra.interpreter.run("print(2 + 2)", { timeout = 30, max_memory_mb = 256, on_output = function(line) print("py: " .. line) end,})if err then error(err) endif result.stdout then print(result.stdout) endcaudra.json
Section titled “caudra.json”JSON encoding, decoding, and schema validation. Encode Lua tables to JSON strings, decode JSON back into tables, and optionally validate data against a JSON Schema.
local s = caudra.json.encode({ ok = true })local t = caudra.json.decode(s)caudra.json.encode()
Section titled “caudra.json.encode()”caudra.json.encode({value})Turn a Lua value into a JSON string. Tables, strings, numbers, booleans, and nil all work. Functions and userdata cannot be serialized.
Parameters:
{value}(any) Lua value to encode.
Returns: (string?, string?) JSON string, or nil plus an error.
Example:
local s, err = caudra.json.encode({ name = "caudra", version = 1 })print(s) -- {"name":"caudra","version":1}caudra.json.decode()
Section titled “caudra.json.decode()”caudra.json.decode({str})Parse a JSON string into a Lua value. Objects become tables and arrays become 1-indexed sequences.
Parameters:
{str}(string) JSON string to decode.
Returns: (any?, string?) Decoded value, or nil plus an error.
Example:
local t, err = caudra.json.decode('{"x": 42}')print(t.x) -- 42caudra.json.schema_validator()
Section titled “caudra.json.schema_validator()”caudra.json.schema_validator({schema})Compile a JSON Schema into a reusable validator object. Supports draft-07, 2019-09, and 2020-12. Schema errors show up right away so you catch mistakes before doing any real work.
Parameters:
{schema}(table) JSON Schema as a Lua table.
Returns: (caudra.json.SchemaValidator?, string?) Validator, or nil plus an error.
Example:
local v, err = caudra.json.schema_validator({ type = "object", properties = { name = { type = "string" } }, required = { "name" },})local errs = v:validate({ name = "caudra" })assert(errs == nil)caudra.json.SchemaValidator
Section titled “caudra.json.SchemaValidator”A compiled JSON Schema validator. Create one with caudra.json.schema_validator() and reuse it to validate many values without recompiling the schema each time.
SchemaValidator:validate()
Section titled “SchemaValidator:validate()”SchemaValidator:validate({value})Check {value} against the compiled schema. Returns nil when the value is valid. When validation fails, returns a list of human-readable error strings.
Parameters:
{value}(any) The Lua value to validate.
Returns: (table?) Array of error strings, or nil if valid.
Example:
local errs = validator:validate({ name = 123 })if errs thenfor _, msg in ipairs(errs) do print(msg) endendcaudra.keymap
Section titled “caudra.keymap”Key mappings, modeled after vim.keymap. If you have written a
Neovim keymap plugin before, this will feel familiar.
caudra.keymap.set("n", "<C-t>", function() print("hello")end, { desc = "Say hello" })caudra.keymap.set()
Section titled “caudra.keymap.set()”caudra.keymap.set({mode}, {lhs}, {rhs}, {opts?})Bind a key to a Lua function, just like vim.keymap.set. Only
normal mode ("n") is supported right now. If {lhs} is already
mapped, the old binding is replaced and a warning is logged.
Prefix {lhs} with <leader> to bind a two-key chord under Ctrl+X.
Leader chords are a separate namespace, so <leader>t and t can both
be bound.
Parameters:
-
{mode}(string) Mode letter. Currently only"n"is accepted. -
{lhs}(string) Key in Vim notation, e.g."<C-t>","<Space>","a","<leader>d". -
{rhs}(function) Called when the key is pressed. -
{opts?}(table?) Options:desc(string) short description shown in the keymap list.
Example:
caudra.keymap.set("n", "<C-t>", function() print("toggle!")end, { desc = "Toggle panel" })caudra.keymap.set("n", "<leader>d", function() print("Ctrl+X then d")end, { desc = "Deploy" })caudra.keymap.del()
Section titled “caudra.keymap.del()”caudra.keymap.del({mode}, {lhs})Remove the mapping for {lhs} in {mode}. Does nothing if no mapping exists for that key.
Parameters:
{mode}(string) Mode letter (reserved for future modes).{lhs}(string) Key to unmap, in Vim notation.
Example:
caudra.keymap.del("n", "<C-t>")caudra.log
Section titled “caudra.log”Structured logging for plugins.
Each call emits a tracing event tagged with the calling plugin's name.
Messages show up in caudra's log output, which you can view with caudra --log.
caudra.log.info("ready")caudra.log.warn("something looks off")caudra.log.debug()
Section titled “caudra.log.debug()”caudra.log.debug({msg})Emit a DEBUG-level log message. Useful for development and troubleshooting. The message is tagged with the plugin name automatically.
Parameters:
{msg}(string) Message to log.
Example:
caudra.log.debug("loaded " .. #items .. " items")caudra.log.info()
Section titled “caudra.log.info()”caudra.log.info({msg})Emit an INFO-level log message. Good for normal operational events.
Parameters:
{msg}(string) Message to log.
Example:
caudra.log.info("plugin initialized")caudra.log.warn()
Section titled “caudra.log.warn()”caudra.log.warn({msg})Emit a WARN-level log message. Use for recoverable problems.
Parameters:
{msg}(string) Message to log.
Example:
caudra.log.warn("config file missing, using defaults")caudra.log.error()
Section titled “caudra.log.error()”caudra.log.error({msg})Emit an ERROR-level log message. Use for failures that need attention.
Parameters:
{msg}(string) Message to log.
Example:
caudra.log.error("failed to connect to API")caudra.model
Section titled “caudra.model”The model behind the focused session. Good for a keybind that flips
between your two go-to models, or lifts thinking for one hard question.
Without an interactive UI every function returns
nil, "no interactive UI attached".
caudra.model.get()
Section titled “caudra.model.get()”caudra.model.get()Reads the focused session's model, thinking level, and fast mode.
thinking comes back in the spelling set accepts, so a table from here
can go straight back in.
Returns: (table|nil, string|nil) {spec, id, provider, thinking, fast, supports_thinking, supports_fast}, or nil and an error.
Example:
local m = caudra.model.get()if m.spec ~= "anthropic/claude-opus-4-6" then ... endcaudra.model.available()
Section titled “caudra.model.available()”caudra.model.available()Lists the model specs you can switch to: what the providers you are logged into offer, minus what your model policy blocks. The list fills in the background at startup, so right after launch it can still be empty.
Returns: (table|nil, string|nil) Array of "provider/id" specs, or nil and an error.
Example:
local specs = caudra.model.available()caudra.model.set()
Section titled “caudra.model.set()”caudra.model.set({opts})Switches the focused session's model, thinking level, or fast mode. Fields
you leave out stay as they are, so this doubles as a thinking-only switch.
Answers with the new state, in the same shape get returns.
Parameters:
-
{opts}(string|table) A model spec, or a table with any of:spec(string)"provider/id", as listed byavailable();thinking(string|number)"off","adaptive", an effort level
(
"minimal"to"max"), a token budget, or""to toggle it on and off;fast(boolean) fast mode, on a model that sells a fast tier.
Returns: (table|nil, string|nil) The new state, or nil and an error.
Example:
caudra.model.set("anthropic/claude-opus-4-6")caudra.model.set({ spec = "zai/glm-5", thinking = "high" })caudra.keymap.set("n", "<M-t>", function() caudra.model.set({ thinking = "" }) end)caudra.net
Section titled “caudra.net”HTTP client for fetching web content. All traffic goes over HTTPS (plain HTTP is upgraded). Private and metadata IP addresses are blocked to prevent SSRF. Failed requests (5xx) are retried automatically.
local res, err = caudra.net.request("https://example.com")if res then print(res.body) endcaudra.net.request()
Section titled “caudra.net.request()”caudra.net.request({url}, {opts?})Make an HTTP request and return the response body. Plain http://
URLs are automatically upgraded to https://. Requests to private
or metadata IP addresses are blocked for safety.
{opts} fields:
method (string) HTTP verb (default "GET").
headers (table) Header name/value pairs.
body (string) Request body.
timeout (integer) Timeout in seconds, max 120 (default 30).
max_bytes (integer) Max response size in bytes (default 5 MB).
retry (integer) Retries on 5xx errors (default 3).
The response table has three fields: body (string), status
(integer), and content_type (string).
Requires the net plugin permission.
Parameters:
{url}(string) URL starting withhttp://orhttps://.{opts?}(table?) Request options (see above).
Returns: (table?, string?) Response table, or nil plus an error string.
Example:
local res, err = caudra.net.request("https://httpbin.org/get")if err then print("failed: " .. err)else print(res.status, res.body)endcaudra.session
Section titled “caudra.session”Host session primitives. The interactive UI can run several sessions
at once; these functions let plugins list, create, focus, rename, and
delete them. Session management returns nil, "no interactive UI attached" without a UI. notify instead targets a live agent mailbox
directly, so it also works under ACP and SDK frontends.
caudra.session.list()
Section titled “caudra.session.list()”caudra.session.list()Lists sessions stored for the current project. Answered from a background scan, so a slow disk never blocks the UI.
Returns: (table|nil, string|nil) Array of {id, title, updated_at}, or nil and an error.
Example:
local stored, err = caudra.session.list()caudra.session.live()
Section titled “caudra.session.live()”caudra.session.live()Lists the sessions currently running in this UI. Status is "working", "needs_input", or "idle". A mailbox follow-up stays "working" without an intermediate "idle" status.
Returns: (table|nil, string|nil) Array of {id, title, status, updated_at, focused}, or nil and an error.
Example:
local live, err = caudra.session.live()caudra.session.current()
Section titled “caudra.session.current()”caudra.session.current()Returns the id of the currently focused session.
Returns: (string|nil, string|nil) Session id, or nil and an error.
Example:
local id = caudra.session.current()caudra.session.focus()
Section titled “caudra.session.focus()”caudra.session.focus({id})Switches the UI to the session with {id}.
Parameters:
{id}(string) Session id, as returned bylist()orlive().
Returns: (boolean|nil, string|nil) true on success, or nil and an error.
Example:
local _, err = caudra.session.focus(id)caudra.session.delete()
Section titled “caudra.session.delete()”caudra.session.delete({id})Deletes a session and its stored history, cancelling it first if it is running. The focused session cannot be deleted.
Parameters:
{id}(string) Session id to delete.
Returns: (boolean|nil, string|nil) true on success, or nil and an error.
Example:
local _, err = caudra.session.delete(id)caudra.session.new()
Section titled “caudra.session.new()”caudra.session.new({opts?})Starts a new session in the current project.
Parameters:
-
{opts?}(table?) Optional fields: prompt (string) first user messageto submit right away; focus (boolean) switch the UI to the new session.
Returns: (string|nil, string|nil) New session id, or nil and an error.
Example:
local id, err = caudra.session.new({ prompt = "fix the tests", focus = true })caudra.session.prompt()
Section titled “caudra.session.prompt()”caudra.session.prompt({text}, {opts?})Sends {text} as a regular user prompt to a live session. The text is
never interpreted: slash commands, exit, and ! shell prefixes are
all sent to the model verbatim. If the session is currently streaming,
admission controls when the prompt is picked up.
Parameters:
-
{text}(string) The prompt to send. Must not be blank. -
{opts?}(table?) Optional fields: session (string) id of a livesession; defaults to the focused one. admission (string) is "queue",
"steer", or "interrupt"; defaults to "queue".
Returns: (string|nil, string|nil) "started", "queued", "steered", or
"replacing", or nil and an error.
Example:
local state, err = caudra.session.prompt("run the tests", { session = id })caudra.session.notify()
Section titled “caudra.session.notify()”caudra.session.notify({text}, {opts?})Reports {text} to a live session without creating a user turn. The observation waits for the session's next agent run.
Parameters:
{text}(string) What to report. Must not be blank.{opts?}(table) Options:session(string) id of a live session.wake(boolean) start a TUI turn when it next becomes idle (default false).
Returns: (boolean|nil, string|nil) true, or nil and an error.
Example:
caudra.session.notify("[monitor] deploy failed", { session = id, wake = true })caudra.session.set_title()
Section titled “caudra.session.set_title()”caudra.session.set_title({opts})Renames a session, live or stored.
Parameters:
{opts}(table) Required fields: id (string) session to rename;title(string) the new title.
Returns: (boolean|nil, string|nil) true on success, or nil and an error.
Example:
local _, err = caudra.session.set_title({ id = id, title = "refactor" })caudra.task
Section titled “caudra.task”The subagents of the focused session and their transcripts. Tasks are
spawned by the task tool and addressed by an id that survives a reload.
Without an interactive UI every function returns
nil, "no interactive UI attached".
caudra.task.list()
Section titled “caudra.task.list()”caudra.task.list()Lists the focused session's chats in chat order. Entry 1 is always the main
chat, with id "main" and no status: its work is the session's own, and
caudra.session.live() already reports that. The rest are subagents, keyed by
the tool call that spawned them.
Returns: (table|nil, string|nil) Array of {id, name, focused, status?} where
status is "working", "done", or "error", or nil and an error.
Example:
for _, t in ipairs(caudra.task.list() or {}) do print(t.name, t.status or "main")endcaudra.task.focus()
Section titled “caudra.task.focus()”caudra.task.focus({id})Shows a task's transcript, the way the chat cycling keys do. An id from another session returns an error instead of landing on the wrong task.
Parameters:
{id}(string) Task id, as returned bylist()."main"is the main chat.
Returns: (boolean|nil, string|nil) true on success, or nil and an error.
Example:
local _, err = caudra.task.focus("main")caudra.text
Section titled “caudra.text”Text transformation utilities.
Helper functions for converting between text formats.
local md = caudra.text.html_to_markdown(html)caudra.text.html_to_markdown()
Section titled “caudra.text.html_to_markdown()”caudra.text.html_to_markdown({html})Convert an HTML string to Markdown.
Useful for cleaning up web content fetched with caudra.webfetch.
Parameters:
{html}(string) HTML source text.
Returns: (string?, string?) Markdown text on success, or nil plus an error message.
Example:
local md, err = caudra.text.html_to_markdown("<h1>Hello</h1><p>world</p>")if err then return endprint(md) -- "# Hello\n\nworld"caudra.treesitter
Section titled “caudra.treesitter”Tree-sitter parsing and query API.
Mirrors vim.treesitter from Neovim, so plugins can be shared between the two.
Start with get_parser() to parse source code, then use get_node_text() and
the query sub-module to extract information from the syntax tree.
local parser, err = caudra.treesitter.get_parser(source, "lua")local trees = parser:parse()local root = trees[1]:root()caudra.treesitter.get_parser()
Section titled “caudra.treesitter.get_parser()”caudra.treesitter.get_parser({source}, {lang})Creates a LanguageTree for {source} using the grammar named {lang}.
This is the main entry point for parsing source code with tree-sitter.
Signature matches vim.treesitter.get_parser(), so Neovim plugins can be copy-pasted.
Parameters:
{source}(string) Source text to parse.{lang}(string) Language name, e.g."rust"or"lua".
Returns: (LanguageTree|nil, string|nil) Parser, or nil and an error message.
Example:
local parser, err = caudra.treesitter.get_parser(src, "lua")if err then print("error: " .. err) endcaudra.treesitter.get_string_parser()
Section titled “caudra.treesitter.get_string_parser()”caudra.treesitter.get_string_parser({source}, {lang})Alias for get_parser. Use whichever name you prefer.
Parameters:
{source}(string) Source text to parse.{lang}(string) Language name.
Returns: (LanguageTree|nil, string|nil) Parser, or nil and an error message.
caudra.treesitter.get_node_text()
Section titled “caudra.treesitter.get_node_text()”caudra.treesitter.get_node_text({node}, {source})Gets the text that {node} covers in {source}. Useful when you have a captured node and need the actual source substring.
Parameters:
{node}(Node) The node whose text you want.{source}(string) Original source text the tree was parsed from.
Returns: (string) Substring covered by the node.
Example:
local text = caudra.treesitter.get_node_text(node, source)print(text)caudra.treesitter.get_node_range()
Section titled “caudra.treesitter.get_node_range()”caudra.treesitter.get_node_range({node})Returns the range of {node} as four 0-based integers: start_row, start_col, end_row, end_col.
Parameters:
{node}(Node) The node to query.
Returns: (integer, integer, integer, integer) start_row, start_col, end_row, end_col.
Example:
local sr, sc, er, ec = caudra.treesitter.get_node_range(node)caudra.treesitter.get_range()
Section titled “caudra.treesitter.get_range()”caudra.treesitter.get_range({node})Returns a six-element table for {node}: {start_row, start_col, start_byte, end_row, end_col, end_byte}.
This gives you byte offsets in addition to row/column positions.
Parameters:
{node}(Node) The node to query.
Returns: (table) Six-element array: start_row, start_col, start_byte, end_row, end_col, end_byte.
Example:
local r = caudra.treesitter.get_range(node)print("bytes: " .. r[3] .. "-" .. r[6])caudra.treesitter.is_ancestor()
Section titled “caudra.treesitter.is_ancestor()”caudra.treesitter.is_ancestor({dest}, {source})Checks whether {dest} is an ancestor of {source} (or the same node). Walks up from {source} toward the root looking for {dest}.
Parameters:
Returns: (boolean)
caudra.treesitter.is_in_node_range()
Section titled “caudra.treesitter.is_in_node_range()”caudra.treesitter.is_in_node_range({node}, {line}, {col})Checks whether the 0-based position ({line}, {col}) falls inside {node}. Handy for cursor-position checks.
Parameters:
{node}(Node) Node to test against.{line}(integer) 0-based line number.{col}(integer) 0-based column number.
Returns: (boolean)
caudra.treesitter.node_contains()
Section titled “caudra.treesitter.node_contains()”caudra.treesitter.node_contains({node}, {range})Checks whether {node} fully contains the given {range}.
Parameters:
{node}(Node) Node to test.{range}(table) Four-element array{start_row, start_col, end_row, end_col}.
Returns: (boolean)
caudra.treesitter.get_node()
Section titled “caudra.treesitter.get_node()”caudra.treesitter.get_node({opts?})Placeholder for cursor-based node lookup (not yet implemented, always returns nil).
Parameters:
{opts?}(table?) Options (currently unused).
Returns: (Node|nil) Always nil.
caudra.treesitter.language
Section titled “caudra.treesitter.language”Language registry for tree-sitter grammars.
Mirrors vim.treesitter.language. Use these functions to register grammars,
map filetypes to languages, and inspect available node types.
caudra.treesitter.language.add("lua")caudra.treesitter.language.register("lua", "luau")caudra.treesitter.language.add()
Section titled “caudra.treesitter.language.add()”caudra.treesitter.language.add({lang}, {opts?})Registers {lang} for use with tree-sitter. Call this to confirm a language grammar is available. Throws if {lang} is unknown. Custom grammar paths are not yet supported.
Parameters:
{lang}(string) Language name, e.g."rust".{opts?}(table?) Options table (thepathkey is not yet supported).
Example:
caudra.treesitter.language.add("lua")caudra.treesitter.language.register()
Section titled “caudra.treesitter.language.register()”caudra.treesitter.language.register({lang}, {filetype})Associates {lang} with one or more filetypes, so you can look up the right
parser language for a given filetype later with get_lang().
Parameters:
{lang}(string) Language name.{filetype}(string|table) A single filetype string or an array of filetype strings.
Example:
caudra.treesitter.language.register("typescript", { "ts", "tsx" })caudra.treesitter.language.get_lang()
Section titled “caudra.treesitter.language.get_lang()”caudra.treesitter.language.get_lang({filetype})Looks up the tree-sitter language name for {filetype}. Returns the registered language, or falls back to {filetype} itself if a grammar with that name exists. Returns nil when nothing matches.
Parameters:
{filetype}(string) Filetype to look up, e.g."ts".
Returns: (string|nil) Language name, or nil.
Example:
local lang = caudra.treesitter.language.get_lang("tsx")if lang then print(lang) end -- "typescript"caudra.treesitter.language.get_filetypes()
Section titled “caudra.treesitter.language.get_filetypes()”caudra.treesitter.language.get_filetypes({lang})Returns all filetypes that have been registered for {lang}.
Parameters:
{lang}(string) Language name.
Returns: (table) Array of filetype strings.
Example:
local fts = caudra.treesitter.language.get_filetypes("typescript")-- { "ts", "tsx" }caudra.treesitter.language.inspect()
Section titled “caudra.treesitter.language.inspect()”caudra.treesitter.language.inspect({lang})Returns metadata about the grammar for {lang}. Useful for debugging or discovering which node types and fields a grammar defines.
Parameters:
{lang}(string) Language name.
Returns: (table) Table with keys abi_version (integer), node_types (string[]), fields (string[]).
Example:
local info = caudra.treesitter.language.inspect("lua")print("ABI: " .. info.abi_version)for _, nt in ipairs(info.node_types) do print(nt) endcaudra.treesitter.query
Section titled “caudra.treesitter.query”Query compilation and lookup.
Mirrors vim.treesitter.query. Use parse() to compile a tree-sitter
query string into a Query object you can run against parsed trees.
local q = caudra.treesitter.query.parse("lua", "(string) @str")caudra.treesitter.query.parse()
Section titled “caudra.treesitter.query.parse()”caudra.treesitter.query.parse({lang}, {query})Compiles a tree-sitter query string for {lang}. Throws if the language is unknown or the query has a syntax error.
Parameters:
{lang}(string) Language name, e.g."lua".{query}(string) Tree-sitter S-expression query.
Returns: (Query) Compiled query object.
Example:
local q = caudra.treesitter.query.parse("lua", "(identifier) @id")caudra.treesitter.query.get()
Section titled “caudra.treesitter.query.get()”caudra.treesitter.query.get({lang}, {name})Looks up a named built-in query for {lang} (not yet implemented, always returns nil).
Parameters:
{lang}(string) Language name.{name}(string) Query name, e.g."highlights".
Returns: (Query|nil) Query object, or nil if not found.
caudra.treesitter.Query
Section titled “caudra.treesitter.Query”A compiled tree-sitter query.
Get one by calling caudra.treesitter.query.parse(lang, query_string).
Then use :iter_captures() or :iter_matches() to run it against a syntax tree.
local q = caudra.treesitter.query.parse("lua", "(identifier) @id")for idx, node, meta in q:iter_captures(root, source) do print(node:type())endQuery:iter_captures()
Section titled “Query:iter_captures()”Query:iter_captures({node}, {source}, {start_row?}, {stop_row?})Iterates over every capture matched by this query. Each call to the returned iterator yields (capture_index, node, metadata, match, active). Use this when you care about individual captures rather than whole pattern matches.
Parameters:
{node}(Node) Root node to search within.{source}(string) Source text the tree was parsed from.{start_row?}(integer) Only match rows >= this value (0-based).{stop_row?}(integer) Only match rows < this value (0-based).
Returns: (function) Iterator yielding (integer, Node, table, table, integer).
Example:
local q = caudra.treesitter.query.parse("lua", "(identifier) @id")for idx, node, meta in q:iter_captures(root, source) do print(idx, node:type())endQuery:iter_matches()
Section titled “Query:iter_matches()”Query:iter_matches({node}, {source}, {start_row?}, {stop_row?})Iterates over every full pattern match in this query. Each call to the returned iterator yields (pattern_index, captures, metadata, active) where captures is a table keyed by capture index. Use this when you need all captures for a pattern together.
Parameters:
{node}(Node) Root node to search within.{source}(string) Source text the tree was parsed from.{start_row?}(integer) Only match rows >= this value (0-based).{stop_row?}(integer) Only match rows < this value (0-based).
Returns: (function) Iterator yielding (integer, table, table, integer).
Example:
local q = caudra.treesitter.query.parse("lua", "(function_declaration name: (identifier) @name)")for pat, captures, meta in q:iter_matches(root, source) do for cap_idx, nodes in pairs(captures) do print(nodes[1]:type()) endendcaudra.treesitter.Tree
Section titled “caudra.treesitter.Tree”A parsed syntax tree.
Obtained from LanguageTree:parse() or LanguageTree:trees().
Call :root() to get the root node and start traversing.
local trees = parser:parse()local root = trees[1]:root()Tree:root()
Section titled “Tree:root()”Tree:root()Returns the root node of this tree. This is where you start walking the syntax tree or running queries.
Returns: (Node) Root node.
Example:
local root = tree:root()print(root:type()) -- e.g. "chunk" for LuaTree:copy()
Section titled “Tree:copy()”Tree:copy()Returns an independent copy of this tree. Edits to the copy will not affect the original.
Returns: (Tree) A new Tree with the same content.
caudra.treesitter.Node
Section titled “caudra.treesitter.Node”A single node in a parsed syntax tree.
Nodes are obtained from Tree:root(), navigation methods like :child(),
or from query captures. Each node knows its type, range, and children.
local root = tree:root()print(root:type(), root:child_count())for child, field in root:iter_children() do print(child:type(), field)endNode:type()
Section titled “Node:type()”Node:type()Returns the grammar type name for this node, like "function_definition" or "identifier".
Returns: (string) Grammar type name.
Node:symbol()
Section titled “Node:symbol()”Node:symbol()Returns the numeric symbol id for this node's grammar type. Two nodes with the same type always share the same symbol id.
Returns: (integer) Symbol id.
Node:id()
Section titled “Node:id()”Node:id()Returns a unique string identifier for this specific node in the tree. Useful for deduplication or as a table key.
Returns: (string) Node identity string.
Node:range()
Section titled “Node:range()”Node:range({include_bytes?})Returns the range of this node as multiple return values.
Without {include_bytes}: start_row, start_col, end_row, end_col.
With {include_bytes} set to true: start_row, start_col, start_byte, end_row, end_col, end_byte.
Parameters:
{include_bytes?}(boolean) When true, byte offsets are included in the return values.
Returns: (integer, integer, integer, integer) Four values, or six when include_bytes is true.
Example:
local sr, sc, er, ec = node:range()local sr, sc, sb, er, ec, eb = node:range(true)Node:start()
Section titled “Node:start()”Node:start()Returns the start position of this node: row, column, and byte offset (all 0-based).
Returns: (integer, integer, integer) start_row, start_col, start_byte.
Node:end_()
Section titled “Node:end_()”Node:end_()Returns the end position of this node: row, column, and byte offset (all 0-based).
Returns: (integer, integer, integer) end_row, end_col, end_byte.
Node:byte_length()
Section titled “Node:byte_length()”Node:byte_length()Returns how many bytes this node spans in the source text.
Returns: (integer) Byte length.
Node:child()
Section titled “Node:child()”Node:child({index})Returns the child at position {index} (0-based), including anonymous nodes like punctuation. Returns nil if {index} is out of bounds.
Parameters:
{index}(integer) 0-based child index.
Returns: (Node|nil) Child node, or nil.
Node:named_child()
Section titled “Node:named_child()”Node:named_child({index})Returns the named child at position {index} (0-based), skipping anonymous nodes. Returns nil if {index} is out of bounds.
Parameters:
{index}(integer) 0-based named child index.
Returns: (Node|nil) Named child node, or nil.
Node:child_count()
Section titled “Node:child_count()”Node:child_count()Returns the total number of children, including anonymous nodes.
Returns: (integer) Child count.
Node:named_child_count()
Section titled “Node:named_child_count()”Node:named_child_count()Returns the number of named children (skipping anonymous punctuation nodes).
Returns: (integer) Named child count.
Node:children()
Section titled “Node:children()”Node:children()Returns all children (named and anonymous) as a Lua table.
Returns: (table) Array of Node.
Example:
for _, child in ipairs(node:children()) do print(child:type())endNode:named_children()
Section titled “Node:named_children()”Node:named_children()Returns all named children as a Lua table, skipping anonymous nodes.
Returns: (table) Array of Node.
Node:iter_children()
Section titled “Node:iter_children()”Node:iter_children()Returns an iterator function that yields (child, field_name) for every child.
The field name is nil for children that are not assigned to a grammar field.
Returns: (function) Iterator yielding (Node, string|nil).
Example:
for child, field in node:iter_children() do if field then print(field .. ": " .. child:type()) endendNode:field()
Section titled “Node:field()”Node:field({name})Returns all children assigned to the grammar field {name} as a table.
For example, a function node might have a "name" or "body" field.
Parameters:
{name}(string) Field name defined in the grammar.
Returns: (table) Array of Node.
Example:
local bodies = node:field("body")Node:parent()
Section titled “Node:parent()”Node:parent()Returns the parent of this node, or nil if this is the root.
Returns: (Node|nil) Parent node.
Node:next_sibling()
Section titled “Node:next_sibling()”Node:next_sibling()Returns the next sibling (named or anonymous), or nil if this is the last child.
Returns: (Node|nil) Next sibling.
Node:prev_sibling()
Section titled “Node:prev_sibling()”Node:prev_sibling()Returns the previous sibling (named or anonymous), or nil if this is the first child.
Returns: (Node|nil) Previous sibling.
Node:next_named_sibling()
Section titled “Node:next_named_sibling()”Node:next_named_sibling()Returns the next named sibling, skipping anonymous nodes. Returns nil at the end.
Returns: (Node|nil) Next named sibling.
Node:prev_named_sibling()
Section titled “Node:prev_named_sibling()”Node:prev_named_sibling()Returns the previous named sibling, skipping anonymous nodes. Returns nil at the start.
Returns: (Node|nil) Previous named sibling.
Node:child_with_descendant()
Section titled “Node:child_with_descendant()”Node:child_with_descendant({descendant})Finds the direct child of this node that contains {descendant}. Returns nil if {descendant} is not actually inside this node.
Parameters:
{descendant}(Node) A node that may be a descendant.
Returns: (Node|nil) Direct child containing the descendant.
Node:descendant_for_range()
Section titled “Node:descendant_for_range()”Node:descendant_for_range({start_row}, {start_col}, {end_row}, {end_col})Finds the smallest node inside this node that spans the given point range. Includes both named and anonymous nodes.
Parameters:
{start_row}(integer) Start row (0-based).{start_col}(integer) Start column (0-based).{end_row}(integer) End row (0-based).{end_col}(integer) End column (0-based).
Returns: (Node|nil) Smallest node covering the range, or nil.
Node:named_descendant_for_range()
Section titled “Node:named_descendant_for_range()”Node:named_descendant_for_range({start_row}, {start_col}, {end_row}, {end_col})Like descendant_for_range, but only considers named nodes.
Parameters:
{start_row}(integer) Start row (0-based).{start_col}(integer) Start column (0-based).{end_row}(integer) End row (0-based).{end_col}(integer) End column (0-based).
Returns: (Node|nil) Smallest named node covering the range, or nil.
Node:named()
Section titled “Node:named()”Node:named()Returns true if this is a named node (not anonymous punctuation like , or ().
Returns: (boolean)
Node:extra()
Section titled “Node:extra()”Node:extra()Returns true if this node is an "extra" (like a comment) that can appear anywhere in the grammar.
Returns: (boolean)
Node:missing()
Section titled “Node:missing()”Node:missing()Returns true if this node is "missing", meaning it was inserted by the parser during error recovery.
Returns: (boolean)
Node:has_error()
Section titled “Node:has_error()”Node:has_error()Returns true if this node or any of its descendants contain a syntax error.
Returns: (boolean)
Node:has_changes()
Section titled “Node:has_changes()”Node:has_changes()Returns true if this node has been marked as changed since the last parse.
Returns: (boolean)
Node:equal()
Section titled “Node:equal()”Node:equal({other})Returns true if this node and {other} are the same node in the tree.
Parameters:
{other}(Node) Node to compare against.
Returns: (boolean)
Node:sexpr()
Section titled “Node:sexpr()”Node:sexpr()Returns the S-expression (lisp-like) string for this node and its children. Handy for debugging the tree structure.
Returns: (string) S-expression.
Example:
print(node:sexpr()) -- e.g. "(identifier)"Node:tree()
Section titled “Node:tree()”Node:tree()Returns the Tree that this node belongs to.
Returns: (Tree) The owning tree.
caudra.treesitter.LanguageTree
Section titled “caudra.treesitter.LanguageTree”Manages parsing of a source string for a single language.
Obtained from caudra.treesitter.get_parser() or caudra.treesitter.get_string_parser().
Call :parse() to get the syntax tree, then use :root() on the tree to start walking nodes.
local parser, err = caudra.treesitter.get_parser(source, "lua")if not err then local trees = parser:parse() local root = trees[1]:root()endLanguageTree:parse()
Section titled “LanguageTree:parse()”LanguageTree:parse({range?})Parses the source and returns a table containing the resulting Tree. The tree is cached, so calling this again is cheap.
Parameters:
{range?}(table) Unused. Accepted for API compatibility.
Returns: (table) Array with one Tree element.
Example:
local trees = parser:parse()local root = trees[1]:root()LanguageTree:lang()
Section titled “LanguageTree:lang()”LanguageTree:lang()Returns the language name this parser was created with.
Returns: (string) Language name, e.g. "lua".
LanguageTree:children()
Section titled “LanguageTree:children()”LanguageTree:children()Returns child LanguageTrees for injected languages. Not yet implemented, always returns an empty table.
Returns: (table) Empty table.
LanguageTree:trees()
Section titled “LanguageTree:trees()”LanguageTree:trees()Returns all parsed trees as a table (at most one for now).
Returns an empty table if parse() has not been called yet.
Returns: (table) Array of Tree.
LanguageTree:source()
Section titled “LanguageTree:source()”LanguageTree:source()Returns the source string this parser was created with.
Returns: (string) The original source text.
LanguageTree:is_valid()
Section titled “LanguageTree:is_valid()”LanguageTree:is_valid({exclude_children?}, {range?})Checks whether the parse tree is still valid. Not yet implemented, always returns true.
Parameters:
{exclude_children?}(boolean) Unused.{range?}(table) Unused.
Returns: (boolean) Always true.
LanguageTree:for_each_tree()
Section titled “LanguageTree:for_each_tree()”LanguageTree:for_each_tree({fn})Calls {fn} with (tree, nil) for the parsed tree.
Triggers a parse if the tree has not been parsed yet.
Parameters:
{fn}(function) Callback receiving(Tree, nil).
Example:
parser:for_each_tree(function(tree, _) print(tree:root():type())end)LanguageTree:included_regions()
Section titled “LanguageTree:included_regions()”LanguageTree:included_regions()Returns the regions this parser covers. Not yet implemented, always returns a table with one empty region.
Returns: (table) Array with one empty table.
LanguageTree:contains()
Section titled “LanguageTree:contains()”LanguageTree:contains({range})Checks whether this parser covers the given {range}. Not yet implemented, always returns true.
Parameters:
{range}(table) Range to check (currently unused).
Returns: (boolean) Always true.
LanguageTree:destroy()
Section titled “LanguageTree:destroy()”LanguageTree:destroy()Drops the cached parse tree and frees its memory.
After calling this, the next parse() will re-parse from scratch.
caudra.ui
Section titled “caudra.ui”Functions for building interactive UI. Create buffers to hold content, open floating or split windows to display them, highlight code, render markdown, and show status hints.
local buf = caudra.ui.buf()buf:line("hello from my plugin!")local win = caudra.ui.open_win(buf, { title = "Greeting", width = "50%", height = 5 })caudra.ui.buf()
Section titled “caudra.ui.buf()”caudra.ui.buf()Creates a new buffer for building UI content. The first buffer you create in a task becomes the "live" buffer, streamed to the UI while your tool runs. Create more buffers for secondary content like floating windows.
Returns: (Buf) Buffer handle.
Example:
local buf = caudra.ui.buf()buf:line("hello world")caudra.ui.theme_color()
Section titled “caudra.ui.theme_color()”caudra.ui.theme_color({name})Looks up a semantic color from the current theme. Use this to keep your plugin's colors consistent with the rest of the UI.
Parameters:
{name}(string) Semantic color name, e.g. "accent" or "background".
Returns: (string|nil) "#rrggbb" hex color, or nil if the name is unknown.
Example:
local accent = caudra.ui.theme_color("accent")if accent then buf:line({ { "note", { fg = accent, bold = true } } })endcaudra.ui.highlight()
Section titled “caudra.ui.highlight()”caudra.ui.highlight({code}, {lang}, {opts?})Syntax-highlights a chunk of source code. Returns a table of styled
lines that you can feed into a buffer. Each line is a list of
{text, style} spans where style is a {fg, bold?, italic?, underline?} table.
Parameters:
{code}(string) Source text to highlight.{lang}(string) Language identifier, e.g. "rust", "python".{opts?}(table?) Options. Fields:independent(boolean) highlight each line without cross-line context. Default false.prefix(string) prepend to the source before highlighting (affects token context). Default "".
Returns: (table) Lines: { { {text, style}, ... }, ... }. Each style is {fg, bold?, italic?, underline?}.
Example:
local lines = caudra.ui.highlight("fn main() {}", "rust")for _, spans in ipairs(lines) do buf:line(spans)endcaudra.ui.markdown()
Section titled “caudra.ui.markdown()”caudra.ui.markdown({text}, {width})Renders Markdown into styled lines ready to display in a buffer.
Each span's style is either a named string ("bold", "heading",
"inline_code", etc.) or a {fg, bold?, italic?, underline?} table
for syntax-highlighted code blocks.
Parameters:
{text}(string) Markdown source.{width}(integer) Wrap width in columns.
Returns: (table) Lines: { { {text, style}, ... }, ... }.
Example:
local size = caudra.ui.terminal_size()local lines = caudra.ui.markdown("# Hello\n\nSome **bold** text.", size.cols)for _, spans in ipairs(lines) do buf:line(spans)endcaudra.ui.humantime()
Section titled “caudra.ui.humantime()”caudra.ui.humantime({secs})Formats a number of seconds into a short, human-friendly string. Useful for displaying elapsed time in status messages.
Parameters:
{secs}(integer) Duration in seconds.
Returns: (string) Human-readable duration, e.g. "1m30s".
Example:
caudra.ui.humantime(90) -- "1m30s"caudra.ui.humantime(3661) -- "1h1m1s"caudra.ui.terminal_size()
Section titled “caudra.ui.terminal_size()”caudra.ui.terminal_size()Returns the current terminal size. Handy for sizing floating windows or wrapping text to fit the screen.
Returns: (table) {cols, rows}, terminal width and height in characters.
Example:
local size = caudra.ui.terminal_size()local half_width = math.floor(size.cols / 2)caudra.ui.display_width()
Section titled “caudra.ui.display_width()”caudra.ui.display_width({text})Returns the display width of a string in terminal cells, matching
how ratatui measures text.
Parameters:
{text}(string) The text to measure.
Returns: (integer) Number of display cells the text occupies.
Example:
local w = caudra.ui.display_width("hello")caudra.ui.truncate_text()
Section titled “caudra.ui.truncate_text()”caudra.ui.truncate_text({text}, {max_width})Splits a string at a display-cell boundary.
Parameters:
{text}(string) The text to split.{max_width}(integer) Maximum display cells for the head.
Returns: (table) {head = string, tail = string}.
Example:
local t = caudra.ui.truncate_text("hello world", 5)-- t.head == "hello", t.tail == " world"caudra.ui.flash()
Section titled “caudra.ui.flash()”caudra.ui.flash({msg})Shows a brief message in the status bar. The message disappears after a short time. Good for confirming an action like "copied!" or showing a transient warning.
Parameters:
{msg}(string) Message text.
Example:
caudra.ui.flash("Copied to clipboard!")caudra.ui.action()
Section titled “caudra.ui.action()”caudra.ui.action({name})Runs a built-in UI action by name, exactly as its default keybinding
would. Handy when a default key never reaches caudra because tmux or
your terminal grabs it first: bind a new key with caudra.keymap.set
and call this from it.
Valid names: "command_palette", "file_picker", "search",
"help", "plan_toggle", "plan_editor", "edit_input",
"pop_queue", "prev_chat", "next_chat", "model_picker",
"copy_message", "review", "view_toggle", "stash_push",
"stash_pop", "stash_list", "workbench".
For slash commands rather than keybound actions, see
caudra.api.run_command.
Parameters:
{name}(string) Action name, e.g."file_picker".
Returns: (boolean|nil, string|nil) true on success, or nil and an error message for an unknown name.
Example:
-- Open the built-in file picker with Ctrl+Q instead of Ctrl+S:caudra.keymap.set("n", "<C-q>", function() caudra.ui.action("file_picker")end)caudra.ui.open_editor()
Section titled “caudra.ui.open_editor()”caudra.ui.open_editor({path})Opens {path} in the user's $EDITOR (e.g. vim, nano) and waits for
it to close. This suspends the TUI while the editor is running.
Returns the editor's exit code so you can check if the user saved.
To open a file without leaving Caudra, use caudra.ui.open_workbench.
Parameters:
{path}(string) File to open.
Returns: (integer) Editor exit code, or -1 if the action could not be dispatched.
Example:
local code = caudra.ui.open_editor("/tmp/scratch.lua")if code == 0 then caudra.ui.flash("File saved")endcaudra.ui.open_workbench()
Section titled “caudra.ui.open_workbench()”caudra.ui.open_workbench({path}, {opts?})Opens {path} in the workbench, Caudra's built-in editor, and returns
straight away. A relative path is resolved against the project root,
the way a clicked @path mention is. The TUI stays up throughout.
Parameters:
{path}(string) File to open.{opts?}(table?)line(integer, 1-based): the line to put the cursor on.
Returns: (boolean|nil, string|nil) true once the UI has the request, or nil and an error message.
Example:
caudra.ui.open_workbench("src/main.rs", { line = 42 })caudra.ui.open_win()
Section titled “caudra.ui.open_win()”caudra.ui.open_win({buf}, {opts})Opens a floating or split window that displays the contents of {buf}. Returns a Win handle you can use to receive events, update layout, and close the window when you are done.
Parameters:
{buf}(Buf) Buffer to display.{opts}(table) Float configuration. Fields:width(integer|string) window width. Integer for absolute columns; "N%" for percent of terminal width. Default "60%".height(integer|string) window height. Integer for absolute rows; "N%" for percent of terminal height. Default "70%".row(integer?) row offset from the anchor corner. Negative values move up.col(integer?) column offset from the anchor corner.anchor(string) corner the (row, col) offset is relative to. One of "NW" (default), "NE", "SW", "SE".border(string) border style. One of "rounded" (default), "single", "double", "none".title(string) text shown in the top border. Default "".title_pos(string) title alignment. One of "left" (default), "center", "right".footer(table) key-hint pairs shown in the bottom border. Each entry is {key, label}.zindex(integer) stacking order. Default 50.cursor_line(boolean) highlight the focused row. Default false.reserved_top(integer) rows reserved at the top of the content area. Default 0.reserved_bottom(integer) rows reserved at the bottom of the content area. Default 0.split(string) dock the window to an edge instead of floating. One of "above", "below", "left", "right", "panel", or "" (floating, default).order(integer) paint order among split windows at the same edge. Default 50.focus(boolean) whether the window takes keyboard focus on open. Default true.visible(boolean) whether the window is initially visible. Default true.needs_input(boolean) whether the window means the session needs user input. Default false.
Returns: (Win) Window handle.
Example:
local buf = caudra.ui.buf()buf:line("Pick an option:")local win = caudra.ui.open_win(buf, { title = "Menu", width = "50%", height = 10, cursor_line = true, footer = { { "q", "quit" }, { "Enter", "select" } },})caudra.ui.set_status_hint()
Section titled “caudra.ui.set_status_hint()”caudra.ui.set_status_hint({spans})Shows key hints in the status bar for your plugin. Each hint is a {key, label} pair. Pass nil to clear your plugin's hints. Only your own hints are affected, other plugins keep theirs.
Parameters:
{spans}(table|nil) Sequence of {key, label} pairs, e.g.{{"q", "quit"}, {"j", "down"}}. Pass nil to remove the plugin's hints.
Example:
caudra.ui.set_status_hint({ {"q", "quit"}, {"j", "down"} })-- later, clear them:caudra.ui.set_status_hint(nil)caudra.ui.set_window_title()
Section titled “caudra.ui.set_window_title()”caudra.ui.set_window_title({title})Sets the terminal emulator's window title. Pass an empty string to clear it.
The title passes through tmux, GNU screen, and zellij untouched, and control characters are stripped, so model text cannot inject escape sequences into the terminal. On exit caudra hands the title back to the shell, on terminals that support the title stack.
Parameters:
{title}(string) New window title, e.g."● 3/5 tests".
Example:
caudra.ui.set_window_title("caudra: " .. session_name)-- Give the title back to the shell:caudra.ui.set_window_title("")caudra.ui.Win
Section titled “caudra.ui.Win”Handle to a floating or split window. You get one from
caudra.ui.open_win(). Use recv() in a loop to handle keyboard
input, and call close() when done.
Fields: width, height (initial content dimensions in columns/rows),
visible (current visibility).
local win = caudra.ui.open_win(buf, { title = "Demo" })while true do local ev = win:recv() if not ev or ev.key == "q" then break endendwin:close()Win:recv()
Section titled “Win:recv()”Win:recv({timeout_ms?})Waits for the next event from this window. Call this in a loop to build an interactive UI. Returns nil once the window is closed or the channel disconnects. Pass {timeout_ms} to also get {type="timeout"} events so your plugin can animate while idle.
Event tables by type:
{type="key", key}-- keypress. Key is a string like "q", "j", or "esc".{type="resize", width, height}-- terminal was resized.{type="paste", text}-- bracketed paste.{type="close"}-- window was closed externally.{type="timeout"}-- no event arrived within {timeout_ms}.
Parameters:
{timeout_ms?}(integer) Max milliseconds to wait before a timeout event is returned.
Returns: (table|nil) Event table, or nil if the window has closed.
Example:
while true do local ev = win:recv() if not ev or ev.key == "q" then break end if ev.type == "key" and ev.key == "j" then -- move cursor down endendwin:close()Win:set_config()
Section titled “Win:set_config()”Win:set_config({opts})Updates the window layout on the fly. Only the fields you include in {opts} are changed, everything else stays the same.
Parameters:
{opts}(table) Partial float config. Accepted fields:title(string) border title text.title_pos(string) title alignment, "left", "center", or "right".footer(table) key-hint pairs{{key, label}, ...}shown in the bottom border.border(string) "rounded", "single", "double", or "none".anchor(string) corner origin, "NW", "NE", "SW", or "SE".width(integer|string) new width; integer or "N%".height(integer|string) new height; integer or "N%".zindex(integer) stacking order.cursor_line(boolean) highlight the focused row.reserved_top(integer) rows reserved at the top of the content area.split(string) edge docking, "above", "below", "left", "right", "panel", or "".order(integer) paint order among split windows.needs_input(boolean) whether the window means the session needs user input.
Example:
win:set_config({ title = "Updated!", width = "80%" })Win:set_cursor()
Section titled “Win:set_cursor()”Win:set_cursor({row})Moves the highlighted cursor line to {row} (1-indexed). Only has a
visible effect when the window was opened with cursor_line = true.
Parameters:
{row}(integer) Target row, 1-indexed.
Example:
win:set_cursor(3) -- highlight the third lineWin:close()
Section titled “Win:close()”Win:close()Closes the window and frees its resources. Safe to call more than once. The window also closes automatically when the handle is garbage collected.
Example:
win:close()Win:is_open()
Section titled “Win:is_open()”Win:is_open()Returns true if the window is still alive (not closed). Useful for checking before sending commands.
Returns: (boolean) true if open.
Example:
if win:is_open() then win:set_config({ title = "still here" })endWin:show()
Section titled “Win:show()”Win:show()Makes the window visible again after it was hidden with hide().
Example:
win:show()Win:hide()
Section titled “Win:hide()”Win:hide()Hides the window without closing it. The window keeps its state
and buffer contents. Call show() to bring it back.
Example:
win:hide()-- do some work...win:show()Win:is_visible()
Section titled “Win:is_visible()”Win:is_visible()Returns true if the window is both open and visible (not hidden).
Returns: (boolean) true if visible.
caudra.ui.Buf
Section titled “caudra.ui.Buf”A content buffer that holds styled lines of text. Create one with
caudra.ui.buf() and pass it to caudra.ui.open_win() to show it in
a floating or split window.
local buf = caudra.ui.buf()buf:line("hello")buf:line({ { "world", "bold" } })Buf:line()
Section titled “Buf:line()”Buf:line({line})Appends a single line to the end of the buffer. You can pass a
plain string for unstyled text, or a table of {text, style?} spans
for rich content. Style can be a named string like "bold" or
"keyword", or an inline table {fg?, bg?, bold?, italic?, underline?, dim?, strikethrough?, reversed?}
with "#rrggbb" color strings.
Parameters:
{line}(string|table) Plain string, or a sequence of spans:{ {text, style?}, ... }.
Example:
buf:line("plain text")buf:line({ { "ERROR", { fg = "#ff0000", bold = true } }, { " something broke" } })Buf:lines()
Section titled “Buf:lines()”Buf:lines({lines})Appends several lines at once. Each entry uses the same format as
buf:line(), so you can mix plain strings and styled spans.
Parameters:
{lines}(table) Sequence of line values, each the same format accepted bybuf:line.
Example:
buf:lines({ "first line", { { "styled ", "bold" }, { "second line" } }, "third line",})Buf:set_lines()
Section titled “Buf:set_lines()”Buf:set_lines({lines})Replaces every line in the buffer with {lines}. Use this when you want to redraw the whole buffer, for example after the user toggles a view.
Parameters:
{lines}(table) Sequence of line values, each the same format accepted bybuf:line.
Example:
buf:set_lines({ "new content", "replaces everything" })Buf:len()
Section titled “Buf:len()”Buf:len()Returns how many lines the buffer currently holds.
Returns: (integer) Line count.
Example:
if buf:len() == 0 then buf:line("(empty)")endBuf:get_lines()
Section titled “Buf:get_lines()”Buf:get_lines()Returns all lines in the buffer as a Lua table. Each line is a
sequence of {text, style?} spans, the same format buf:line()
accepts. Useful for reading back content, copying it to another
buffer, or round-tripping through set_lines().
Returns: (table) Sequence of lines.
Example:
local lines = buf:get_lines()buf:set_lines(lines) -- round-tripBuf:on()
Section titled “Buf:on()”Buf:on({event}, {callback})Registers an event handler on the buffer.
Supported events:
- "click": fires when the user clicks a line. The handler receives a click-event table and may yield or mutate the buffer.
- "change": fires synchronously after every mutation (
line,lines,set_lines). Must not yield.
Calling on() again for the same event replaces the previous handler.
Parameters:
{event}(string) Event name: "click" or "change".{callback}(function) Handler function. For "click", receives a click-event table. For "change", receives no arguments.
Example:
buf:on("click", function(ev) caudra.ui.flash("Clicked row " .. ev.row)end)Buf:click()
Section titled “Buf:click()”Buf:click({ev})Programmatically fires the buffer's click handler with event {ev}. Does nothing if no click handler is registered. Useful for testing or simulating user interaction from code.
Parameters:
{ev}(table) Click event table passed to the handler.
Example:
buf:click({ row = 1 })Buf:blit()
Section titled “Buf:blit()”Buf:blit({fb}, {width}, {height}, {opts?})Replaces the whole buffer with a pixel frame drawn as "▀" cells.
Each cell's foreground is the top pixel and its background the
bottom one, so one text line fits two pixel rows. When {height} is
odd the last line leaves its background unset and the terminal
default shows through.
{fb} is a Luau buffer of raw pixel bytes in row-major order,
top-left origin. Its size must be exactly
width * height * bytes_per_pixel for the chosen format, otherwise
the call throws. A mismatch usually means a wrong width or format,
and an early error beats hunting down a garbled frame.
Formats: "rgb" is the default at 3 bytes per pixel. "rgba" and
"bgra" take 4 bytes per pixel and ignore the 4th byte. "bgra" is
what a little-endian uint32 holding 0xRRGGBB looks like in
memory, the layout doomgeneric uses for its framebuffer.
char swaps the "▀" glyph for another one column wide string,
e.g. "█" when only the foreground color should show. The
foreground still comes from the top pixel and the background from
the bottom one, whatever the glyph.
Parameters:
{fb}(buffer) Raw pixel bytes.{width}(integer) Frame width in pixels, > 0.{height}(integer) Frame height in pixels, > 0.{opts?}(table|nil) Options:format= "rgb"|"rgba"|"bgra",char= one column wide string.
Example:
local fb = buffer.create(160 * 100 * 3)buffer.writeu8(fb, (y * 160 + x) * 3, 255) -- red channelbuf:blit(fb, 160, 100)buf:blit(fb32, 160, 100, { format = "bgra", char = "█" })caudra.uv
Section titled “caudra.uv”System and environment utilities, modelled after vim.uv.
Provides access to the working directory, home directory, and environment variables. None of these functions throw.
local home = caudra.uv.os_homedir()caudra.uv.cwd()
Section titled “caudra.uv.cwd()”caudra.uv.cwd()Return the current working directory as an absolute path. Like vim.uv.cwd.
Requires the env plugin permission.
Returns: (string?) Current working directory, or nil if it cannot be determined.
Example:
local cwd = caudra.uv.cwd()if cwd then print("working in: " .. cwd) endcaudra.uv.os_homedir()
Section titled “caudra.uv.os_homedir()”caudra.uv.os_homedir()Return the current user's home directory. Like vim.uv.os_homedir.
Requires the env plugin permission.
Returns: (string?) Home directory path, or nil if it cannot be determined.
Example:
local home = caudra.uv.os_homedir() -- e.g. "/home/user"caudra.uv.os_getenv()
Section titled “caudra.uv.os_getenv()”caudra.uv.os_getenv({name})Look up the environment variable {name}. Like vim.uv.os_getenv.
Returns nil when the variable is not set.
Requires the env plugin permission.
Parameters:
{name}(string) Name of the environment variable.
Returns: (string?) Variable value, or nil if not set.
Example:
local editor = caudra.uv.os_getenv("EDITOR") or "vi"caudra.yaml
Section titled “caudra.yaml”YAML encoding and decoding. Works the same way as caudra.json,
but for YAML formatted strings.
local t = caudra.yaml.decode("greeting: hello")print(t.greeting)caudra.yaml.encode()
Section titled “caudra.yaml.encode()”caudra.yaml.encode({value})Turn a Lua value into a YAML string. Most Lua types work, but circular references will return an error.
Parameters:
{value}(any) Lua value to encode.
Returns: (string?, string?) YAML string, or nil plus an error.
Example:
local s, err = caudra.yaml.encode({ name = "caudra", tags = { "ai", "agent" } })print(s)caudra.yaml.decode()
Section titled “caudra.yaml.decode()”caudra.yaml.decode({str})Parse a YAML string into a Lua value. Mappings become tables and sequences become 1-indexed arrays.
Parameters:
{str}(string) YAML string to decode.
Returns: (any?, string?) Decoded value, or nil plus an error.
Example:
local t, err = caudra.yaml.decode("name: caudra\nversion: 1")print(t.name) -- caudraShared helper modules
Section titled “Shared helper modules”These ship inside caudra; require them from any plugin. Small modules are
shown as full source, larger ones as their public interface.
require("caudra.color")
Section titled “require("caudra.color")”local M = {}
function M.lerp(from, to, t) local fr, fg, fb = from:match("#(%x%x)(%x%x)(%x%x)") local tr, tg, tb = to:match("#(%x%x)(%x%x)(%x%x)") if not fr or not tr then return from end fr, fg, fb = tonumber(fr, 16), tonumber(fg, 16), tonumber(fb, 16) tr, tg, tb = tonumber(tr, 16), tonumber(tg, 16), tonumber(tb, 16) local r = math.floor(fr + (tr - fr) * t + 0.5) local g = math.floor(fg + (tg - fg) * t + 0.5) local b = math.floor(fb + (tb - fb) * t + 0.5) return string.format("#%02x%02x%02x", r, g, b)end
function M.dim(color, factor) local bg = caudra.ui.theme_color("background") or "#000000" return M.lerp(color, bg, factor)end
return Mrequire("caudra.dir_listing")
Section titled “require("caudra.dir_listing")”-- Shared directory listing for index and list plugins.-- Lists entries, filters instruction files, sorts dirs before files, and-- renders the listing so every caller shows a directory the same way.function M.list(path, ctx)function M.view(text, ctx)require("caudra.fuzzy_replace")
Section titled “require("caudra.fuzzy_replace")”M.NO_MATCH = "old_string not found in file"M.MULTIPLE_MATCHES = "old_string matches multiple locations; add surrounding context to make it unique"M.EMPTY_OLD_STRING = "old_string must not be empty"
-- Replace {old_string} with {new_string} in {content}, tolerating small-- whitespace and indentation drift. Returns the new content, or nil plus-- one of the error constants above.function M.replace(content, old_string, new_string, replace_all)require("caudra.list_picker")
Section titled “require("caudra.list_picker")”-- Draws the filter query and its blank spacer into {lines}, pins that height on-- {win} and returns it, which is also the first scrollable line. Drawing and-- pinning belong together: a query that wraps, or one pasted with a newline,-- makes the header taller than a picker would guess, and a reserved_top guessed-- elsewhere then mis-scrolls the list.function ListPicker.render_header(win, lines, input, prefix, inner)
-- Open a fuzzy-filter picker in a floating window and block until the user-- decides. {items} is a list of strings or { label, detail? } tables. {opts}:-- title, footer, cursor (initial index), submit_keys (extra submit keys-- besides enter). Returns { type = "choice"|"delete", index } or-- { type = "close" }.function ListPicker.open(items, opts)ListPicker.split_words = split_wordsListPicker.matches = matchesListPicker.highlight_spans = highlight_spansrequire("caudra.output_limits")
Section titled “require("caudra.output_limits")”-- Shared per-tool output limit options, so the tools that support them-- cannot drift apart.M.DEFAULT_MAX_LINE_BYTES = DEFAULT_MAX_LINE_BYTESfunction M.extend(spec)
--- Returns max_lines, max_bytes: tool override when set, agent-wide otherwise.function M.resolve(opts, ctx)require("caudra.partial")
Section titled “require("caudra.partial")”-- When a tool is cut short, it still hands back what it printed. The marker-- tells the model that output is real but unfinished. One home for the-- wording and the painting, so every tool says it the same way.
--- Close {view} on the marker and build the tool reply. {out} is everything--- the tool streamed; empty means the view still shows a placeholder to drop.--- {reason} is a cancel-hook reason ("cancelled" |--- "timeout").function M.cut(view, out, reason, timeout_secs)require("caudra.scroll")
Section titled “require("caudra.scroll")”-- Relative scrolling on top of caudra.fn.winsaveview / winrestview.-- Positive {delta} scrolls down, negative up. Returns (true, nil) or (nil, err).local function scroll(delta) local view, err = caudra.fn.winsaveview() if not view then return nil, err end return caudra.fn.winrestview({ topline = view.topline + delta })end
return scrollrequire("caudra.shorten_path")
Section titled “require("caudra.shorten_path")”local function normalize_sep(s) return s:gsub("\\", "/")end
local function shorten_path(path) local p = normalize_sep(path) local cwd = caudra.uv.cwd() if cwd then cwd = normalize_sep(cwd) if p:sub(1, #cwd + 1) == cwd .. "/" then local rel = p:sub(#cwd + 2) return rel == "" and "." or rel end end local home = caudra.uv.os_homedir() if home then home = normalize_sep(home) if p:sub(1, #home + 1) == home .. "/" then local rel = p:sub(#home + 2) return rel == "" and "~" or "~/" .. rel end end return pathend
return shorten_pathrequire("caudra.test_helpers")
Section titled “require("caudra.test_helpers")”-- Shared test helpers for Lua plugin specs.---- Provides a lightweight test harness: `case` wraps each block in pcall so a-- single failure does not abort the rest of the suite. Failures are collected-- and surfaced by `report()` at the end.function M.case(name, fn)function M.eq(actual, expected, msg)function M.has(s, substr, msg)function M.mktmpdir(prefix)function M.rmtree(dir)function M.report()require("caudra.text_input")
Section titled “require("caudra.text_input")”-- TextInput: multi-line editable buffer with a byte-offset cursor.---- Invariants enforced everywhere:-- * `line` is 1-based and indexes a line that always exists.-- * `col` is a byte offset inside `lines[line]`, always on a UTF-8 codepoint-- boundary, so `lines[line]:sub(1, col)` is a complete UTF-8 prefix.-- * No line ever contains a literal newline; newlines split into rows.---- Parents OWN their keys. `handle_key` returns one of R.IGNORED / R.MOVED /-- R.CHANGED. Parent dispatchers must filter their own keys (esc, ctrl+c,-- submit keys, etc.) BEFORE forwarding, because `handle_key` claims any key-- it can interpret. `ctrl+a` is bound to move-home; if a parent wants it for-- "select all" it must intercept first.---- IGNORED is returned when the buffer literally cannot act (backspace at-- (1, 0), right at end of buffer, etc.). Parents can use that signal to fall-- through to their own logic.---- Parity cases live in plugins/lib/tests/spec.lua (TRACE_CASES). Add one-- whenever you change handle_key semantics.TextInput.Result = Rfunction TextInput.new()function TextInput:value()function TextInput:is_empty()function TextInput:line_count()function TextInput:clear()
-- Returns the codepoint right before the cursor as a string, or nil at the-- start of a line. Lets callers peek backwards (e.g. "is the previous char-- a backslash?") without touching internal indices.function TextInput:char_before_cursor()function TextInput:insert_text(text)function TextInput:insert_char(c)function TextInput:insert_space()function TextInput:split_line()function TextInput:remove_char()function TextInput:delete_char()function TextInput:remove_word_before()function TextInput:delete_word_after()function TextInput:kill_to_end_of_line()function TextInput:move_left()function TextInput:move_right()function TextInput:move_up()function TextInput:move_down()function TextInput:move_home()function TextInput:move_end()function TextInput:move_word_left()function TextInput:move_word_right()function TextInput:handle_key(key)
-- Wrap lines to {width} with {prefix} before the first row. Returns-- { lines = styled lines, cursor_row = 1-based row holding the cursor }.function TextInput:render(prefix, prefix_width, width)require("caudra.tool_view")
Section titled “require("caudra.tool_view")”-- The shared truncate/expand body that tool plugins render through.---- Click handlers get `ev.row`, a 1-based line in this buf; 0 means the-- click landed outside it (the header). The handler lives on the buf-- itself, so any wrapper of the same buf (a batch child's foreign handle)-- reaches the same toggle. Expansion is never stored: the UI records-- clicked rows and replays them through `restore` in order, so `toggle`-- stays a pure flag flip + re-render, deterministic across replays.-- Async highlighting goes through `caudra.async.run`; during restore the-- runtime runs those tasks inline before snapshotting.
-- Right aligned, so the content column stays put when a number gains a digit.-- Formats the number alone, callers add their own separator.function ToolView.line_nr_fmt(max_line_nr)
-- opts: max_lines (default 80) shown while collapsed, keep "head"|"tail"-- (default "tail"), max_expand_lines (default 2000) kept for expansion,-- max_line_bytes (optional) per-line byte cap applied at render time.function ToolView.new(buf, opts)function ToolView:set_header(lines)function ToolView:clear()function ToolView:append(line)function ToolView:append_text(text)
-- Append {content} with line numbers, then syntax-highlight it for {ext}-- asynchronously. Returns false when {content} is empty.function ToolView:set_highlight(content, ext)
-- Content rows on screen, for callers with their own per-row click targets. A-- single hidden line is drawn as itself instead of a notice, so it counts as-- content too. Rows line up with `all_lines` under keep = "head"; keep = "tail"-- prints its notice first and shifts them.function ToolView:visible_count()function ToolView:toggle()function ToolView:flush()function ToolView:update_line(all_idx, line)
-- Call once after the last append so the collapsed notice renders.function ToolView:finish()function ToolView.restore_lines(lines, opts)
-- Rebuild a collapsed view from a tool's saved llm_output, click-to-toggle-- wired. For `restore` hooks.function ToolView.restore(output, opts)
-- Same, for tools whose live output goes through markdown (`format =-- "markdown"`); {opts.width} is the wrap width. Errors stay plain, as they do-- live.function ToolView.restore_markdown(output, is_error, opts)require("caudra.truncate")
Section titled “require("caudra.truncate")”-- Truncate before host-managed output. Use only when producer-level loss is intentional.-- tool handlers should normally return complete llm_output.local function truncate(text, max_lines, max_bytes) if #text <= max_bytes then local n = 0 for _ in text:gmatch("\n") do n = n + 1 end if n + 1 <= max_lines then return text end end local out = {} local bytes = 0 local lines = 0 for line in text:gmatch("([^\n]*)\n?") do lines = lines + 1 if lines > max_lines then break end local new_bytes = bytes + #line + 1 if new_bytes > max_bytes then break end out[#out + 1] = line bytes = new_bytes end local result = table.concat(out, "\n") if #result < #text then result = result .. "\n\n[truncated " .. (#text - #result) .. " bytes]" end return resultend
return truncate