Editor setup
The whole Lua API is annotated in a kebab.lua definitions file that ships with your
installation — LuaLS type annotations covering every global,
every client method, and every value object. Attaching it gets you completion, hover docs, and
type errors on a file you have not run yet.
It is worth the five minutes. The sandbox has no os, no io, and no way to print a stack
trace, so a typo you catch in the editor is a typo you do not spend a farm run chasing.
require itThat file is ---@meta. It declares types and contains no implementation. Requiring it from
a plugin will not work, and the sandbox has no filesystem searcher anyway. Attach it as a
library, which is a different mechanism.
Find the file
Look for a definitions folder alongside the rest of your installation; it contains
kebab.lua and a small config.json that records the Lua runtime version the annotations are
written for. Everything below points at the folder, not at kebab.lua itself.
If you cannot find it, paths.Names() and scripts eval still let you explore the API from
the console — but you will be doing it by hand.
VS Code
Install the Lua extension by sumneko, then add to your workspace settings, pointing
Lua.workspace.library at the folder containing kebab.lua:
{
"Lua.workspace.library": [
"/path/to/kebab-definitions"
],
"Lua.runtime.version": "Lua 5.1",
"Lua.diagnostics.globals": ["plugin"]
}
Use an absolute path — that way the setting keeps working no matter which directory you have open as your workspace, including a plugins directory that lives nowhere near the definitions file.
Lua 5.1 is correct — gopher-lua implements 5.1. Set this wrong and 5.2+ syntax like goto
labels, integer division (//), and bitwise operators will parse cleanly in your editor and
then fail at load time.
plugin is listed as a known global because your file assigns it at file scope. The
annotations do declare it, so this is belt and braces; leave it in if you also want to silence
the "assigning to an undefined global" diagnostic.
Neovim
With lazydev.nvim or a direct lua_ls setup:
require('lspconfig').lua_ls.setup {
settings = {
Lua = {
runtime = { version = 'Lua 5.1' },
workspace = {
library = { '/path/to/kebab-definitions' },
checkThirdParty = false,
},
diagnostics = { globals = { 'plugin' } },
},
},
}
Same rule as VS Code: use the absolute path to the folder containing kebab.lua, not one
relative to your editor's working directory.
Check that it worked
Open a plugin file and type:
local c = clients:First()
c:
You should get a completion list containing GetZone, IsInCombat, TeleportWithRecovery
and a few dozen more. If you get nothing, the library path is wrong — it is almost always a
relative path, or a path pointing at kebab.lua rather than at its folder.
A second check: hover utils.Ticks. You should see its doc comment about deriving waits from
the host's tick interval. No hover means the file is being read as a workspace source file
rather than as a library, which happens if you put the definitions folder inside your
plugins folder — do not, or the loader will also try to load kebab.lua as a plugin and fail
it.
Annotating your own plugin
Start every file with the plugin type so the editor knows what the table is:
---@type KebabPlugin
plugin = {
name = 'my_bot',
version = '1.0.0',
}
Local helpers benefit from annotations too, particularly around clients and entities where the types are rich:
---@param c Client
---@return number
local function health_pct(c)
local max = c:GetMaxHealth()
if max <= 0 then return 100 end
return (c:GetHealth() / max) * 100
end
---@param c Client
---@return Entity|nil
local function pick_target(c)
return c.entity:Nearest({ name = 'Troubled Warrior', tag = 'mob' })
end
Because state is rebuilt by a reset() function in most plugins, LuaLS infers its shape
from the assignment inside reset — so a field you only ever set elsewhere shows up as an
error. Declare the whole shape in one place and you get completion on state. for free:
---@class FarmState
---@field enabled boolean
---@field phase 'hunt'|'fight'|'settle'|'recover'|'sweep'
---@field wait integer
---@field collected table<string, boolean>
---@type FarmState
local state = {}
What the annotations buy you
The types encode real constraints that are easy to get wrong otherwise:
EquipmentSlotis a closed set of nine strings. Typo"hats"and you find out in the editor rather than after a ten-minute farm run.Client:ZoneChunksreturnsPosition[]|nil, string|nil— two values, second is an error. The annotation makes the second value visible so you do not silently drop it.requireis overloaded per module name, sorequire('equipment')is typed as the equipment module andrequire('pet')is not. Opt-in sub-objects likec.inventoryare annotated withrequires require("inventory"), which is the only place that dependency is written down.TargetStrategyincludesClientAlias, documenting that"p1"is legal anywhere a target is accepted.ClientKeyNamelists exactly the key namesClient:SendKeyunderstands — and, by omission, tells you the function keys are not among them.
Keeping the annotations honest
The definitions file is maintained by hand and can occasionally drift from what the runtime actually does. The automated check that guards it only asserts that each name exists; it does not check how many values a function returns, so return arity is the detail most likely to be stale.
If the editor and the runtime disagree, the runtime is right. Confirm with a utils.Log,
or with the console's scripts eval for a quick one-off:
scripts eval local c = clients:First(); local a, b = c:PotionCount(); utils.Log(tostring(a) .. '/' .. tostring(b))
Treat any mismatch as worth reporting. A wrong annotation is a wrong API as far as anyone with an LSP attached is concerned.