Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

tools

Public tools.* surface — discovery, authoring, and inspection of code-mode tools. Installed as the global tools table by the prelude (via --!global tools).

Tools in this engine are on-disk assets: a <name>.tool/ folder inside a <box>.toolbox/ parent, callable as tools.<box>.<name>(...). A tool's metadata comes from four sources in the folder — the --!desc/--!arg/ --!return/--!example docstring above its function in init.luau, the typed function signature, the sibling .metadata JSON (tags), and README.md. The engine parses the same four sources to bind each tool and render its docs page, so the filesystem IS the registry — there's no runtime registry to keep in sync.

Exports

  • tools.toolboxes() -> { stdout, value } — the toolbox-first overview. One row per toolbox: { toolbox, purpose, toolCount }, purpose sourced from the toolbox README.md (first descriptive line) or its .metadata description. The "what domains exist" entry point — drill in with tools.search("", { toolbox = "<name>" }).
  • tools.list(tier: (number | string)?, toolbox: string?) -> { stdout, value } — grouped discovery. Tier 1 (default): names only. Tier 2: + one-line descriptions. Tier 3: + the assembled signature / args / returns / examples. A string in either slot is treated as a toolbox name.
  • tools.search(query: string?, opts?) -> { stdout, value, total? }: discovery over the tool family, answered by @builtin::modules.discovery. opts.mode picks how a query ranks: "semantic" by what a tool means, "keyword" by the words it carries, where a hit falls by what the query's words the asset carried weigh (a word few assets carry outweighs several nearly every asset carries), then by the role that matched, then by name. A call naming no mode runs discovery.defaultMode(): semantic wherever a world is bound, keyword where none is. The search is discovery.searchTools, the question search_tools { query } asks: not in keyword mode on a bound engine, the search core ranks the tools, toolboxes and workflows of the whole published library, then this world's own matches follow (from = "world"). opts: toolbox, limit, mode. An empty query lists that toolbox's tools, or the catalogue when none is named. Each row's identity (and name) is the <box>.<tool> call path; assetType says whether a tool, a toolbox or a workflow matched, registered whether the registry holds the function a call would run, and matched the role, file, word counts and weight behind a query hit. total, on a query naming no toolbox, is how many matched in all, of which value holds the best limit. A query row also carries guid, installed (whether this world holds the hit), use (the call that acts on it: tool_exec { script: "return await tools.use('<toolbox>', '<tool>')" } for a tool and for the workflow toolbox's start, a search_tools { toolbox } drill-in, or, for a hit this world does not hold, bash { command: "zm ls-tree @<guid>" }, which reads its files), install (the zeromind.installAsset call, on a hit this world does not hold, which brings it in to run on the job) and p when the search core judged it.
  • tools.createToolbox(args) -> { ok, path?, identity?, error? } — scaffold a new .toolbox/. Required: name, description. Optional: sharedCode (override default shared.module/init.luau), into (a .package ref to nest the toolbox inside).
  • tools.create(args) -> { ok, path?, identity?, signature?, error? } — author a new tool inside an existing toolbox. Required: name, toolbox, code, description. Optional: args, returns, examples, tags.
  • tools.delete(identity: string) -> { ok, path?, error? } — delete the .tool/ folder for "<toolbox>.<name>".
  • tools.get(identity: string) -> ToolMeta? — the tool's assembled metadata ({ name, signature?, description, args, returns?, examples, tags }), or nil if missing.
  • tools.parseCommandLine(identity: string, words: { string }, opts: { cwd: string? }?) -> BindResult — read a command line against the tool's own schema (--!flag and --!rest annotations, parameter names, the fields of the trailing options table) and bind it through tools.bind. Each word is read against the type of the slot it reaches: string and Path as written, number and boolean strictly, a union of string literals against its members, anything else as JSON, true/false/null, a number, a comma run of numbers (0,5,0), or text; a word there that starts with { or [ and does not parse is refused bad-value, naming the word and the parse error. Path slots resolve against opts.cwd (default /zero). A refusal carries reason unknown-flag, missing-value, bad-value or given-twice (or one of tools.bind's) and a message written for a shell.
  • tools.callable(toolbox: string, tool: string) -> string? — the identity a call names, "<toolbox>.<tool>", or nil when the registry carries no such tool. Reads the bound toolbox proxies, so a .tool folder whose source never registered a function answers nil. What a surface holding a tool's ASSET uses to say how it is called.
  • tools.render(identity: string, result: any) -> { out: string, err: string?, exit: number? }? — run the tool folder's render.luau (a module returning (result) -> string | { out, err?, exit? }) over a result. A string return becomes { out = s }; nil when the tool holds no render.luau; raises with the render's own message when it raises or returns another type. The module is required at call time, so an edit takes effect on the next call. tools.get(identity).hasRender states whether the file exists.

Usage

-- Discovery
print(tools.list().stdout)         -- summary: toolbox lines, names only
print(tools.list(2).stdout)        -- tier 2: + first-line descriptions
print(tools.list("entityOps").stdout)     -- only the `entityOps` toolbox, summary
print(tools.list("entityOps", 3).stdout)  -- only `entityOps`, full metadata per tool

-- Authoring (two steps — toolbox first, then tools inside it)
local tb = tools.createToolbox({
    name        = "mytools",
    description = "Demo tooling for greeting agents.",
})
assert(tb.ok, tb.error)

local r = tools.create({
    name        = "hello",
    toolbox     = "mytools",
    description = "Greet a name.",
    code        = "return 'hello, ' .. (who or 'world')",
    args = {
        { name = "who", type = "string", description = "Name to greet." },
    },
    returns  = { type = "string", description = "The greeting." },
    examples = { "mytools.hello('agent')" },
    tags     = { "demo" },
})
assert(r.ok, r.error)

-- Inspection — assembled metadata for one tool
print(tools.get("mytools.hello").signature)

-- Removal — deletes the .tool/ folder
tools.delete("mytools.hello")

Notes

  • Auto-installed as the global tools via --!global tools — user code doesn't require this module directly in normal use.
  • Per the project principle "tools in Luau, capabilities in Rust", no FFI calls live here — asset.* and vfs.* already cover the underlying capabilities, and tools never needed a separate runtime registry once they became proper on-disk assets.
  • tools.createToolbox refuses to clobber an existing toolbox. To extend one, edit files via vfs.write and use tools.delete per tool.
  • Every toolbox ships a shared.module/ that holds the helpers its tools share. The init.luau generated by tools.create automatically declares local shared = require(".shared") so the code you pass can reference the module's exports directly. The require is skipped when the toolbox is missing shared.module/, to avoid a guaranteed runtime error.
  • The asset index can lag disk for a frame or two after vfs.remove — tools.list silently skips stale entries to keep discovery clean.