{
  "catalogVersion" : 1,
  "types" : [ {
    "name" : "Assert",
    "kind" : "library",
    "description" : "Checks conditions in a script and fails with a readable message when they do not hold.\n\nA failed check throws an error, which ends the script unless it is caught with `pcall` or the check runs inside `Retry.run`. Every check returns the value it checked, so it can be used inline (`local id = Assert.notNil(response.id)`). The optional last argument is a message that is put in front of the standard text: `my check: Expected 1 but was 2`.",
    "library" : "Assert",
    "category" : "utils",
    "variable" : "Assert",
    "functions" : [ {
      "name" : "that",
      "description" : "Fails with `Condition was not true` unless the value is truthy. In Lua only `false` and `nil` are falsy: `0` and the empty string pass.",
      "returns" : {
        "kind" : "any",
        "description" : "The checked value, returned unchanged so the call can be used inline"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "any",
          "description" : "Condition to check",
          "name" : "condition"
        } ]
      }, {
        "params" : [ {
          "kind" : "any",
          "description" : "Condition to check",
          "name" : "condition"
        }, {
          "kind" : "string",
          "description" : "Text put in front of the failure message",
          "name" : "message"
        } ]
      } ],
      "examples" : [ {
        "code" : "local value = Assert.that(1 + 1 == 2, \"math still works\")\nAssert.equals(4, 2 * 2)\nAssert.contains(\"hello world\", \"world\")",
        "runnable" : true
      } ]
    }, {
      "name" : "equals",
      "description" : "Fails with `Expected x but was y` unless both values are equal. Tables are compared by content, including nested tables, regardless of the order of their keys. Numbers are compared as numbers (`1` equals `1.0`) and a number equals text that reads as the same number (`1` equals `\"1\"`); a boolean never equals a number. Tables in the failure message are shown up to three levels deep.",
      "returns" : {
        "kind" : "any",
        "description" : "The actual value, returned unchanged so the call can be used inline"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "any",
          "description" : "The value you expect",
          "name" : "expected"
        }, {
          "kind" : "any",
          "description" : "The value you got",
          "name" : "actual"
        } ]
      }, {
        "params" : [ {
          "kind" : "any",
          "description" : "The value you expect",
          "name" : "expected"
        }, {
          "kind" : "any",
          "description" : "The value you got",
          "name" : "actual"
        }, {
          "kind" : "string",
          "description" : "Text put in front of the failure message",
          "name" : "message"
        } ]
      } ]
    }, {
      "name" : "notNil",
      "description" : "Fails with `Expected a non-nil value` when the value is `nil`. `false` passes.",
      "returns" : {
        "kind" : "any",
        "description" : "The checked value, returned unchanged so the call can be used inline"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "any",
          "description" : "Value to check",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "any",
          "description" : "Value to check",
          "name" : "value"
        }, {
          "kind" : "string",
          "description" : "Text put in front of the failure message",
          "name" : "message"
        } ]
      } ]
    }, {
      "name" : "isNil",
      "description" : "Fails with `Expected nil but was ...` unless the value is `nil`. `false` fails.",
      "returns" : {
        "kind" : "nil"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "any",
          "description" : "Value to check",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "any",
          "description" : "Value to check",
          "name" : "value"
        }, {
          "kind" : "string",
          "description" : "Text put in front of the failure message",
          "name" : "message"
        } ]
      } ]
    }, {
      "name" : "contains",
      "description" : "Fails unless the text contains the other text. Both values are converted to text first, so numbers work too; the comparison is case-sensitive. A `nil` haystack fails.",
      "returns" : {
        "kind" : "any",
        "description" : "The checked value, returned unchanged so the call can be used inline"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text that should contain `needle`",
          "name" : "haystack"
        }, {
          "kind" : "string",
          "description" : "Text it must contain",
          "name" : "needle"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Text that should contain `needle`",
          "name" : "haystack"
        }, {
          "kind" : "string",
          "description" : "Text it must contain",
          "name" : "needle"
        }, {
          "kind" : "string",
          "description" : "Text put in front of the failure message",
          "name" : "message"
        } ]
      } ]
    } ]
  }, {
    "name" : "Codex",
    "kind" : "library",
    "description" : "Lets a script hand work to the OpenAI Codex CLI (`codex exec`): Codex.task runs one instruction, Codex.session keeps a conversation going over several messages. Use it to let an AI agent analyse, generate or change files in the project and then check the result in the script.\n\nThings to know: the CLI runs on the machine of the Reflex agent. It must be installed there, on the PATH of the agent process (or set REFLEX_CODEX_COMMAND), and already be signed in; Reflex passes no credentials. Check with Codex.available(). It runs non-interactively in the project workspace of the current run with the workspace-write sandbox and approvals disabled, so it can change files there without asking. Prompts and file contents are processed by an external AI service. Arguments are positional (use nil to skip one). A run is killed after 30 minutes unless you give a timeout. A CLI that fails or times out does not raise an error: check result.success (exit is -1 on a timeout). Errors are raised for a missing CLI, a bad argument or an invalid reasoning value.",
    "library" : "Codex",
    "category" : "ai",
    "variable" : "Codex",
    "functions" : [ {
      "name" : "available",
      "description" : "Returns true when the codex command can be found on the PATH of the agent process (or at the path in REFLEX_CODEX_COMMAND). Does not check that it is signed in.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "version",
      "description" : "Returns the first line printed by `codex --version`. Raises an error when the CLI is missing or prints nothing within 10 seconds.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every Codex session of this script and stops Codex processes that are still running for it. The conversations stay in the CLI's own history; sessions that were closed cannot send again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "session",
      "description" : "Starts a Codex conversation: runs `codex exec --json` with the prompt, waits for it to finish and returns the session table. Later messages go to the same conversation through session.send (using `codex exec resume <id>`), so Codex remembers the earlier messages and the files it already looked at. The returned table has the functions send, id and close and the fields sessionId, last (the result of the latest message, see AgentSessionResult) and closed. Raises an error when the CLI does not report a session id.",
      "returns" : {
        "kind" : "object",
        "ref" : "CodexSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The instruction for the agent, a non-empty string",
          "name" : "prompt"
        }, {
          "kind" : "string",
          "description" : "Model name handed to the CLI; nil uses the CLI's configured default",
          "name" : "model",
          "optional" : true
        }, {
          "kind" : "string",
          "description" : "\"low\", \"medium\" or \"high\" (anything else is an error); nil uses the CLI default",
          "name" : "reasoning",
          "optional" : true
        }, {
          "kind" : "number",
          "description" : "Kill the CLI when it runs longer than this many seconds, must be above 0; default 1800 (30 minutes)",
          "name" : "timeoutSeconds",
          "optional" : true
        } ]
      } ]
    }, {
      "name" : "task",
      "description" : "Runs one instruction through `codex exec` and waits for it to finish. The result is a table, not text: success (exit code 0), exit, text (the final answer of Codex), all (the complete output including progress lines), and duration (milliseconds). Each task starts with a fresh context; use Codex.session for follow-up questions. With showReasoning true Codex also produces reasoning summaries and its progress output is printed to the script console while it runs.",
      "returns" : {
        "kind" : "shape",
        "ref" : "AgentTaskResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The instruction for the agent, a non-empty string",
          "name" : "prompt"
        }, {
          "kind" : "string",
          "description" : "Model name handed to the CLI; nil uses the CLI's configured default",
          "name" : "model",
          "optional" : true
        }, {
          "kind" : "string",
          "description" : "\"low\", \"medium\" or \"high\" (anything else is an error); nil uses the CLI default",
          "name" : "reasoning",
          "optional" : true
        }, {
          "kind" : "number",
          "description" : "Kill the CLI when it runs longer than this many seconds, must be above 0; default 1800 (30 minutes)",
          "name" : "timeoutSeconds",
          "optional" : true
        }, {
          "kind" : "boolean",
          "description" : "Include the reasoning output in the console log",
          "name" : "showReasoning",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local result = Codex.task(\"Read scripts/login.lua and list any step that has no assertion. Reply as a bullet list.\")\nif not result.success then\n    error(\"Codex failed (exit \" .. result.exit .. \"): \" .. result.all)\nend\nprint(result.text)",
        "description" : "Ask for a review and fail the script when Codex fails"
      }, {
        "code" : "-- model, reasoning effort and a 10 minute limit; nil keeps the CLI default for the model\nlocal result = Codex.task(\"Add a smoke test for the checkout page to tests/checkout.lua\", nil, \"high\", 600)\nprint(result.success, result.duration .. \" ms\")",
        "description" : "Let Codex write a file in the project, with a time limit"
      } ]
    } ]
  }, {
    "name" : "CodexSession",
    "kind" : "object",
    "description" : "A Codex conversation started by Codex.session. The first prompt has already been answered when you get it (see last); send continues the same thread, so Codex remembers earlier messages.",
    "library" : "Codex",
    "category" : "ai",
    "variable" : "chat",
    "howToGet" : [ {
      "type" : "Codex",
      "function" : "session",
      "call" : "Codex.session(prompt)",
      "returns" : "CodexSession"
    } ],
    "returnedBy" : [ {
      "type" : "Codex",
      "function" : "session"
    } ],
    "functions" : [ {
      "name" : "send",
      "description" : "Sends the next message of the conversation and waits for the answer. Returns the result table, which is also stored as session.last. Raises an error when the session is closed.",
      "returns" : {
        "kind" : "shape",
        "ref" : "AgentSessionResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The next message in the conversation",
          "name" : "prompt"
        } ]
      } ],
      "examples" : [ {
        "code" : "local chat = Codex.session(\"Read tests/checkout.lua and remember its structure.\")\nlocal answer = chat.send(\"Which steps are missing assertions?\")\nprint(answer.text)\nchat.close()"
      } ]
    }, {
      "name" : "id",
      "description" : "Returns the Codex thread id of this conversation, the same value as the sessionId field.",
      "returns" : {
        "kind" : "string",
        "description" : "The session id, nil until the CLI has reported it",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes this session and stops its Codex process if one is still running. Afterwards send raises an error.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Concurrent",
    "kind" : "library",
    "description" : "Runs several Lua functions at the same time on worker threads and waits until all of them are done.\n\nThe functions share the global variables of the script and nothing is synchronised, so keep them independent and let each write to its own variable or table slot. Browser sessions and other library objects are not made thread-safe by this library.",
    "library" : "Concurrent",
    "category" : "concurrency",
    "variable" : "Concurrent",
    "functions" : [ {
      "name" : "run",
      "description" : "Starts a pool of `threads` worker threads, runs every function of the table on it and blocks until all have finished. Functions are called without arguments, their results are discarded, and entries of the table that are not functions are ignored. With fewer threads than functions the rest waits for a free thread.\n\nA function that fails does not stop the others and does not raise an error here: its error is collected instead. The call returns two values, a boolean that is true when nothing failed and a list with one text per failure. That text is the plain error message, such as `script.lua:3: message`, without Java class names. The order of the list is the order in which the failures happened.\n\nThrows when `threads` is not a whole number (`First parameter must be a integer`), when it is below 1, or when the second argument is not a table.",
      "returns" : {
        "kind" : "boolean",
        "description" : "Two values: true when no function failed, and a list of error texts (empty when none failed)"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Number of worker threads, a whole number of at least 1",
          "name" : "threads"
        }, {
          "kind" : "table",
          "description" : "List of Lua functions to run, like `{ fnA, fnB }`",
          "name" : "functions"
        } ]
      } ],
      "examples" : [ {
        "code" : "local ok, errors = Concurrent.run(2, {\n    function() print(\"first\") end,\n    function() print(\"second\") end,\n})\nprint(ok, #errors)",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Copilot",
    "kind" : "library",
    "description" : "Lets a script hand work to the GitHub Copilot CLI (`copilot -p`): Copilot.task runs one instruction, Copilot.session keeps a conversation going over several messages. Use it to let an AI agent analyse, generate or change files in the project and then check the result in the script.\n\nThings to know: the CLI runs on the machine of the Reflex agent. It must be installed there, on the PATH of the agent process (or set REFLEX_COPILOT_COMMAND), and already be signed in with a Copilot subscription; Reflex passes no credentials. Check with Copilot.available(). It runs non-interactively with --allow-all-tools and --no-ask-user, so it may run any shell command and change files without asking, and the project workspace of the current run is added as its working folder. Prompts and file contents are processed by an external AI service. Arguments are positional (use nil to skip one). A run is killed after 30 minutes unless you give a timeout. A CLI that fails or times out does not raise an error: check result.success (exit is -1 on a timeout). Errors are raised for a missing CLI, a bad argument or an invalid reasoning value.",
    "library" : "Copilot",
    "category" : "ai",
    "variable" : "Copilot",
    "functions" : [ {
      "name" : "available",
      "description" : "Returns true when the copilot command can be found on the PATH of the agent process (or at the path in REFLEX_COPILOT_COMMAND). Does not check that it is signed in.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "version",
      "description" : "Returns the first line printed by `copilot --version`. Raises an error when the CLI is missing or prints nothing within 10 seconds.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every Copilot session of this script and stops Copilot processes that are still running for it. The conversations stay in the CLI's own history; sessions that were closed cannot send again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "session",
      "description" : "Starts a Copilot conversation: runs `copilot -p <prompt> --output-format json`, waits for it to finish and returns the session table. Later messages go to the same conversation through session.send (using `--resume=<id>`), so Copilot remembers earlier messages. The returned table has the functions send, id and close and the fields sessionId, last (the result of the latest message, see AgentSessionResult) and closed. Raises an error when the CLI does not report a session id.",
      "returns" : {
        "kind" : "object",
        "ref" : "CopilotSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The instruction for the agent, a non-empty string",
          "name" : "prompt"
        }, {
          "kind" : "string",
          "description" : "Model name passed to the CLI as --model; nil uses the CLI's configured default",
          "name" : "model",
          "optional" : true
        }, {
          "kind" : "string",
          "description" : "\"low\", \"medium\" or \"high\" (anything else is an error); nil uses the CLI default",
          "name" : "reasoning",
          "optional" : true
        }, {
          "kind" : "number",
          "description" : "Kill the CLI when it runs longer than this many seconds, must be above 0; default 1800 (30 minutes)",
          "name" : "timeoutSeconds",
          "optional" : true
        } ]
      } ]
    }, {
      "name" : "task",
      "description" : "Runs one instruction through `copilot -p` and waits for it to finish. The result is a table, not text: success (exit code 0), exit, text (the last answer message of Copilot), all (the complete output: JSON event lines, or readable text with showReasoning), and duration (milliseconds). Each task starts with a fresh context; use Copilot.session for follow-up questions.",
      "returns" : {
        "kind" : "shape",
        "ref" : "AgentTaskResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The instruction for the agent, a non-empty string",
          "name" : "prompt"
        }, {
          "kind" : "string",
          "description" : "Model name passed to the CLI as --model; nil uses the CLI's configured default",
          "name" : "model",
          "optional" : true
        }, {
          "kind" : "string",
          "description" : "\"low\", \"medium\" or \"high\" (anything else is an error); nil uses the CLI default",
          "name" : "reasoning",
          "optional" : true
        }, {
          "kind" : "number",
          "description" : "Kill the CLI when it runs longer than this many seconds, must be above 0; default 1800 (30 minutes)",
          "name" : "timeoutSeconds",
          "optional" : true
        }, {
          "kind" : "boolean",
          "description" : "true to print the agent's reasoning and answers to the script console while it works, and to make all a readable text instead of raw JSON events; default false",
          "name" : "showReasoning",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local result = Copilot.task(\"Read scripts/login.lua and list any step that has no assertion. Reply as a bullet list.\")\nif not result.success then\n    error(\"Copilot failed (exit \" .. result.exit .. \"): \" .. result.all)\nend\nprint(result.text)",
        "description" : "Ask for a review and fail the script when Copilot fails"
      }, {
        "code" : "-- model, reasoning effort, 10 minute limit and live output in the console\nlocal result = Copilot.task(\"Add a smoke test for the checkout page to tests/checkout.lua\", nil, \"high\", 600, true)\nprint(result.success, result.duration .. \" ms\")",
        "description" : "Let Copilot write a file in the project, with a time limit"
      } ]
    } ]
  }, {
    "name" : "CopilotSession",
    "kind" : "object",
    "description" : "A Copilot conversation started by Copilot.session. The first prompt has already been answered when you get it (see last); send continues the same thread, so Copilot remembers earlier messages.",
    "library" : "Copilot",
    "category" : "ai",
    "variable" : "chat",
    "howToGet" : [ {
      "type" : "Copilot",
      "function" : "session",
      "call" : "Copilot.session(prompt)",
      "returns" : "CopilotSession"
    } ],
    "returnedBy" : [ {
      "type" : "Copilot",
      "function" : "session"
    } ],
    "functions" : [ {
      "name" : "send",
      "description" : "Sends the next message of the conversation and waits for the answer. Returns the result table, which is also stored as session.last. Raises an error when the session is closed.",
      "returns" : {
        "kind" : "shape",
        "ref" : "AgentSessionResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The next message in the conversation",
          "name" : "prompt"
        } ]
      } ],
      "examples" : [ {
        "code" : "local chat = Copilot.session(\"Read tests/checkout.lua and remember its structure.\")\nlocal answer = chat.send(\"Which steps are missing assertions?\")\nprint(answer.text)\nchat.close()"
      } ]
    }, {
      "name" : "id",
      "description" : "Returns the Copilot session id of this conversation, the same value as the sessionId field.",
      "returns" : {
        "kind" : "string",
        "description" : "The session id, nil until the CLI has reported it",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes this session and stops its Copilot process if one is still running. Afterwards send raises an error.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Couch",
    "kind" : "library",
    "description" : "Talks to the CouchDB server that is configured in the project settings (URL, user name and password under Project settings > CouchDB in Reflex). The four functions map to the HTTP verbs, and every path is relative to that URL, so Couch.get(\"mydb/doc-1\") requests URL/mydb/doc-1.\n\nThings to know: every function returns a table with status, json and table. Non-2xx statuses such as 404 or 409 (revision conflict) are returned, not raised; only connection failures raise an error. When a user name or password is configured, every request carries Basic authentication (a missing user name or password counts as empty); there is no way to use other credentials from Lua. Updating or deleting a document needs its current revision (_rev in the body, or ?rev= for delete). A table passed as body is encoded as JSON.",
    "library" : "Couch",
    "category" : "databases",
    "variable" : "Couch",
    "functions" : [ {
      "name" : "get",
      "description" : "Sends a GET request to CouchDB: reads a document, a view, _all_docs, database info and so on. The second argument is optional and, when given, is sent as a JSON body (rarely needed for GET). Only the characters [ ] space and \" are escaped in the path, so URL-encode other special characters yourself.",
      "returns" : {
        "kind" : "shape",
        "ref" : "CouchResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        }, {
          "kind" : "table",
          "description" : "Request body: a table that is sent as JSON (application/json)",
          "name" : "table"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Couch.get(\"mydb/order-1001\")\nAssert.equals(200, doc.status)\nprint(doc.table.customer, doc.table.total)\n\nlocal all = Couch.get(\"mydb/_all_docs?include_docs=true&limit=10\")\nfor _, row in ipairs(all.table.rows) do\n    print(row.id)\nend",
        "description" : "Read a document and list documents"
      } ]
    }, {
      "name" : "put",
      "description" : "Sends a PUT request to CouchDB: creates a document under a chosen id, or updates it when the body contains the current _rev (otherwise CouchDB answers 409). Also used to create a database (Couch.put(\"newdb\")). On success the response table has the new revision as table.rev.",
      "returns" : {
        "kind" : "shape",
        "ref" : "CouchResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        }, {
          "kind" : "table",
          "description" : "Request body: a table that is sent as JSON (application/json)",
          "name" : "table"
        } ]
      } ],
      "examples" : [ {
        "code" : "local created = Couch.put(\"mydb/order-1001\", { type = \"order\", customer = \"ACME\", total = 42.5 })\nAssert.equals(201, created.status)\n\nlocal current = Couch.get(\"mydb/order-1001\")\nlocal updated = Couch.put(\"mydb/order-1001\", {\n    _rev = current.table._rev,\n    type = \"order\",\n    customer = \"ACME\",\n    total = 45,\n})\nAssert.equals(201, updated.status)"
      } ]
    }, {
      "name" : "post",
      "description" : "Sends a POST request to CouchDB: creates a document with a generated id (POST to the database), or runs a query such as mydb/_find (Mango) or mydb/_bulk_docs. The body table is sent as JSON.",
      "returns" : {
        "kind" : "shape",
        "ref" : "CouchResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        }, {
          "kind" : "table",
          "description" : "Request body: a table that is sent as JSON (application/json)",
          "name" : "table"
        } ]
      } ],
      "examples" : [ {
        "code" : "local found = Couch.post(\"mydb/_find\", { selector = { type = \"order\", total = { [\"$gt\"] = 40 } }, limit = 20 })\nAssert.equals(200, found.status)\nfor _, doc in ipairs(found.table.docs) do\n    print(doc._id, doc.total)\nend"
      } ]
    }, {
      "name" : "delete",
      "description" : "Sends a DELETE request to CouchDB: deletes a document (the current revision is required as ?rev=REV in the path) or a whole database (Couch.delete(\"mydb\")). The status is 200, or 409 when the revision is outdated.",
      "returns" : {
        "kind" : "shape",
        "ref" : "CouchResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Path and query string relative to the configured CouchDB URL, for example mydb/doc-1 or mydb/_all_docs?limit=10",
          "name" : "query"
        }, {
          "kind" : "table",
          "description" : "Request body: a table that is sent as JSON (application/json)",
          "name" : "table"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Couch.get(\"mydb/order-1001\")\nlocal deleted = Couch.delete(\"mydb/order-1001?rev=\" .. doc.table._rev)\nAssert.equals(200, deleted.status)"
      } ]
    } ]
  }, {
    "name" : "Csv",
    "kind" : "library",
    "description" : "Reads delimited text files (CSV) into Lua tables.\n\nThe reader is deliberately simple: every line is split on the delimiter, quotes are not interpreted (a quoted value that contains the delimiter is split in two) and a line break inside a value is not supported. The first line is not treated specially, so a header row is just row 1. A leading byte order mark is removed and the text is read in the JVM default charset (UTF-8 on current Java versions).",
    "library" : "Csv",
    "category" : "files",
    "variable" : "Csv",
    "functions" : [ {
      "name" : "read",
      "description" : "Reads a CSV file and returns all rows, including the header row, as lists of text values.\n\nEach value is trimmed. The delimiter is a regular expression, not a plain character: `;`, `,` and a tab (`\"\\t\"`) work as expected, but a pipe must be written `\"\\\\|\"` (a bare `|` splits between every character). Empty cells at the end of a line are dropped, so rows can have different lengths, and an empty line becomes a row with one empty value. Throws `Failed to read csv` when the file cannot be read.",
      "returns" : {
        "kind" : "table",
        "description" : "List of rows; each row is a list of strings"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "CSV file to read, relative to the workspace",
          "name" : "fileLocation"
        }, {
          "kind" : "string",
          "description" : "Delimiter as a regular expression, for example `;` or `,` (required)",
          "name" : "delimiter"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"people.csv\", \"name;city\\nAlice;Utrecht\\nBob;Delft\")\nlocal rows = Csv.read(\"people.csv\", \";\")\nprint(rows[2][1], rows[2][2])",
        "runnable" : true
      } ]
    }, {
      "name" : "readAddress",
      "description" : "Reads an address list from a CSV file and returns a helper that serves one random address at a time. The file needs a header row (it is never picked) followed by lines of `street and number`, `zip code`, `city`. The delimiter is a regular expression, as in `Csv.read`.\n\nMeant for test data; see `ListAddress` for how the parts are extracted.",
      "returns" : {
        "kind" : "object",
        "ref" : "ListAddress"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "CSV file to read, relative to the workspace",
          "name" : "fileLocation"
        }, {
          "kind" : "string",
          "description" : "Delimiter as a regular expression, for example `;`",
          "name" : "delimiter"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"addresses.csv\", \"street;zip;city\\nMain Street 12a;1234 AB;Utrecht\\nSide Road 5;5678 CD;Delft\")\nlocal addresses = Csv.readAddress(\"addresses.csv\", \";\")\nprint(addresses.street(), addresses.number(), addresses.zipcode(), addresses.city())",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "ListAddress",
    "kind" : "object",
    "description" : "A random address from the list read by `Csv.readAddress`, split into street, number, number addition, zip code and city.\n\nThe first call to any part function picks a random data row (never the header row) and keeps it: all following calls return parts of that same address. `scramble` forgets it so the next call picks a new one. The file needs the header row and at least one data row; a file with only a header line or no lines throws an error when a part is requested. Column 1 is street and number (`Main Street 12a`), column 2 the zip code and column 3 the city.",
    "library" : "Csv",
    "category" : "files",
    "variable" : "listAddress",
    "howToGet" : [ {
      "type" : "Csv",
      "function" : "readAddress",
      "call" : "Csv.readAddress(fileLocation, delimiter)",
      "returns" : "ListAddress"
    } ],
    "returnedBy" : [ {
      "type" : "Csv",
      "function" : "readAddress"
    } ],
    "functions" : [ {
      "name" : "number",
      "description" : "Returns the digits of the street column with everything else removed (`Main Street 12a` gives `12`).",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "numberAddon",
      "description" : "Returns the text after the house number in the street column (`Main Street 12a` gives `a`), or an empty string when there is none.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "zipcode",
      "description" : "Returns the zip code column with every character removed that is not a letter, digit or whitespace.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "street",
      "description" : "Returns the street column with the digits removed (`Main Street 12a` gives `Main Street a`).",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "city",
      "description" : "Returns the city column of the current address, trimmed.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "scramble",
      "description" : "Forgets the current address so the next part function picks a new random one. Returns the address helper itself, so it can be chained: `addresses.scramble().city()`.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Dashboard",
    "kind" : "library",
    "description" : "Builds the dashboard of a run: headers, paragraphs, lists, tables, dividers and charts that the server shows on the dashboard tab of the run.\n\nEvery function appends one block after the previous ones and returns `Dashboard`, so calls can be chained. Blocks cannot be removed or reordered. The dashboard is stored as JSON when the run ends and `Dashboard.json` returns that same JSON.\n\nText is given as a text table `{ value = \"text\", color = \"red\", background = \"#eee\", size = 14 }` in which only `value` is required. `color` and `background` are CSS colors and `size` is a font size in pixels. In lists and tables, give the items as a plain Lua list (`{ {value = \"a\"}, {value = \"b\"} }`): entries with a non-numeric key are rejected.",
    "library" : "Dashboard",
    "category" : "api",
    "variable" : "Dashboard",
    "functions" : [ {
      "name" : "d",
      "description" : "Adds a divider: a horizontal line, with the text in it when text is given. Without text it is a plain line. Only the text color and the size (in pixels) can be set; there is no background.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Text shown in the divider; empty for a plain line",
          "name" : "text"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Text shown in the divider; empty for a plain line",
          "name" : "text"
        }, {
          "kind" : "string",
          "description" : "Text color as a CSS color, like `red` or `#ff8800`",
          "name" : "color"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Text shown in the divider; empty for a plain line",
          "name" : "text"
        }, {
          "kind" : "string",
          "description" : "Text color as a CSS color, like `red` or `#ff8800`",
          "name" : "color"
        }, {
          "kind" : "number",
          "description" : "Font size in pixels",
          "name" : "size"
        } ]
      } ],
      "examples" : [ {
        "code" : "Dashboard.h({ value = \"Nightly results\" }).p({ value = \"All checks passed\" })\nprint(Dashboard.json())",
        "runnable" : true
      } ]
    }, {
      "name" : "h",
      "description" : "Adds a header. The text must be a text table, a plain string is rejected with `First parameter must be a text table`. The level is 1 (largest) to 6; a value below 1 becomes 1 and a value above 6 throws. The default is 1. The server shows levels 1 to 5 as `h1` to `h5`; level 6 is currently shown as level 1.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "DashboardTextOptions",
          "description" : "The header text",
          "name" : "text"
        } ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "DashboardTextOptions",
          "description" : "The header text",
          "name" : "text"
        }, {
          "kind" : "number",
          "description" : "Header level from 1 (largest) to 6",
          "name" : "size"
        } ]
      } ]
    }, {
      "name" : "p",
      "description" : "Adds a paragraph of text. The text must be a text table; a plain string is rejected.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "DashboardTextOptions",
          "description" : "The paragraph text",
          "name" : "text"
        } ]
      } ]
    }, {
      "name" : "l",
      "description" : "Adds a list. The items are text tables in a plain Lua list; entries with a non-numeric key throw. The second argument marks the list as ordered (`true`) or unordered (`false`, the default) and must be a boolean, otherwise the call throws. The server shows ordered lists with numbers and unordered lists with bullets.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "table",
          "description" : "List of text tables, one per item",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "table",
          "description" : "List of text tables, one per item",
          "name" : "value"
        }, {
          "kind" : "boolean",
          "description" : "true for an ordered list, false for an unordered one",
          "name" : "ordered"
        } ]
      } ],
      "examples" : [ {
        "code" : "Dashboard.l({ { value = \"login works\" }, { value = \"checkout fails\", color = \"red\" } })\nprint(Dashboard.json())",
        "runnable" : true
      } ]
    }, {
      "name" : "t",
      "description" : "Adds a table. The header cells and every cell of every row are text tables. A row may have fewer cells than there are headers but not more (that throws `Cell size ... can't be bigger than headers size`); entries of `rows` that are not tables are skipped.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "table",
          "description" : "List of text tables for the header cells",
          "name" : "headers"
        }, {
          "kind" : "table",
          "description" : "List of rows; each row is a list of text tables, like `{ { {value = \"a\"}, {value = \"b\"} } }`",
          "name" : "values"
        } ]
      } ],
      "examples" : [ {
        "code" : "Dashboard.t(\n    { { value = \"Check\" }, { value = \"Result\" } },\n    {\n        { { value = \"login\" }, { value = \"PASS\", color = \"green\" } },\n        { { value = \"checkout\" }, { value = \"FAIL\", color = \"red\" } },\n    })\nprint(Dashboard.json())",
        "runnable" : true
      } ]
    }, {
      "name" : "c",
      "description" : "Adds one or more charts, drawn in the browser with Chart.js. Pass a list of chart entries; each entry is a table with `height` and `width` (numbers; the server uses them as a percentage of the viewport height and width) and `chart`, a Chart.js configuration table with `type`, `data` and optionally `options`. The `chart` table is converted to JSON, so it can only hold strings, numbers, booleans and tables.\n\nThrows when the argument or an entry is not a table, when `height` or `width` is not a number, or when `chart` is not a table.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "table",
          "description" : "List of chart entries `{ height, width, chart }`",
          "name" : "charts"
        } ]
      } ],
      "examples" : [ {
        "code" : "Dashboard.c({\n    {\n        height = 40,\n        width = 30,\n        chart = {\n            type = \"bar\",\n            data = {\n                labels = { \"passed\", \"failed\" },\n                datasets = { { label = \"Checks\", data = { 12, 3 } } },\n            },\n        },\n    },\n})\nprint(Dashboard.json())",
        "runnable" : true
      } ]
    }, {
      "name" : "json",
      "description" : "Returns the dashboard built so far as pretty-printed JSON, the same text that is stored when the run ends. It is an object whose keys are the block positions (`\"0\"`, `\"1\"`, ...) in call order; with no blocks it is an empty object.",
      "returns" : {
        "kind" : "string",
        "description" : "The dashboard as JSON text"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Data",
    "kind" : "library",
    "description" : "Collects the result data of a run in one table.\n\nWhat you put in with `Data.set` is encoded as JSON when the run ends and stored with the run log, where the server shows it as the data of that run. The table is empty (`{}`) when nothing was set. Because the table is encoded like `Json.encode`, it may only hold strings, numbers, booleans and nested tables; a value JSON cannot hold makes the encoding fail.",
    "library" : "Data",
    "category" : "api",
    "variable" : "Data",
    "functions" : [ {
      "name" : "set",
      "description" : "Sets run data. With one argument, the given table replaces all data collected so far; the table is used as it is, not copied, so later changes to it are part of the data. With two arguments, one key is set; setting a key to `nil` removes it.\n\nWith a single argument that is not a table the call throws.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "table",
          "description" : "Table that becomes the run data, replacing the current one",
          "name" : "luaTable"
        } ]
      }, {
        "params" : [ {
          "kind" : "any",
          "description" : "Key, normally a string",
          "name" : "key"
        }, {
          "kind" : "any",
          "description" : "Value to store: string, number, boolean or table",
          "name" : "value"
        } ]
      } ],
      "examples" : [ {
        "code" : "Data.set(\"status\", \"ok\")\nData.set(\"count\", 3)\nprint(Data.json())",
        "runnable" : true
      } ]
    }, {
      "name" : "clear",
      "description" : "Removes all run data collected so far.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "get",
      "description" : "Returns the data table itself (not a copy), so changes to it change the run data.",
      "returns" : {
        "kind" : "table",
        "description" : "The current run data"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "json",
      "description" : "Returns the run data encoded as pretty-printed JSON, the same text that is stored when the run ends. Throws for values JSON cannot hold, as `Json.encode` does.",
      "returns" : {
        "kind" : "string",
        "description" : "The run data as JSON text"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Dates",
    "kind" : "library",
    "description" : "Formats, shifts and compares dates that are passed around as text.\n\nPatterns use Joda-Time letters: `yyyy` year, `MM` month, `dd` day, `HH` hour (0-23), `hh` hour (1-12) with `a` for AM/PM, `mm` minutes, `ss` seconds, `EEEE` day name, `MMMM` month name, `Z` zone offset; put literal letters in single quotes (`yyyy-MM-dd'T'HH:mm`). A date without a time is midnight in the time zone of the agent. Day and month names follow the language set with `Dates.locale`, by default that of the agent.\n\nWhen dates are shifted the order is days, weeks, months, years, and a day that does not exist in the target month moves to the end of that month (31-01-2024 plus one month is 29-02-2024). A date that does not fit its pattern, or does not exist (31-02-2024), throws.",
    "library" : "Dates",
    "category" : "dates",
    "variable" : "Dates",
    "functions" : [ {
      "name" : "today",
      "description" : "Returns today's date, and optionally the time, formatted as text, in the time zone of the agent. Without arguments the pattern is `dd-MM-yyyy`. Five arguments shift the date by a number of days, weeks, months and years (negative numbers go back; pass 0 to skip one).\n\nUse exactly 0, 1 or 5 arguments: with 2, 3 or 4 arguments only the pattern is used and the shifts are silently ignored.",
      "returns" : {
        "kind" : "string",
        "description" : "Today's date in the requested pattern"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Output pattern, for example `yyyy-MM-dd HH:mm`",
          "name" : "pattern"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Output pattern, for example `yyyy-MM-dd HH:mm`",
          "name" : "pattern"
        }, {
          "kind" : "number",
          "description" : "Days to add (negative to subtract)",
          "name" : "day"
        }, {
          "kind" : "number",
          "description" : "Weeks to add (negative to subtract)",
          "name" : "week"
        }, {
          "kind" : "number",
          "description" : "Months to add (negative to subtract)",
          "name" : "month"
        }, {
          "kind" : "number",
          "description" : "Years to add (negative to subtract)",
          "name" : "year"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Dates.today(\"yyyy-MM-dd\"))\nprint(Dates.format(\"31-12-2024\", \"dd-MM-yyyy\", \"yyyy/MM/dd\"))\nprint(Dates.daysBetween(\"01-01-2024\", \"31-01-2024\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "format",
      "description" : "Converts a date from one pattern to another, optionally shifting it on the way. Seven arguments shift the date by days, weeks, months and years (negative numbers go back; 0 skips one).\n\nUse exactly 3 or 7 arguments: with 4, 5 or 6 arguments the shifts are silently ignored. Throws when the date does not fit the `from` pattern.",
      "returns" : {
        "kind" : "string",
        "description" : "The date in the `to` pattern"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The date as text",
          "name" : "date"
        }, {
          "kind" : "string",
          "description" : "Pattern the date is written in",
          "name" : "from"
        }, {
          "kind" : "string",
          "description" : "Pattern of the result",
          "name" : "to"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The date as text",
          "name" : "date"
        }, {
          "kind" : "string",
          "description" : "Pattern the date is written in",
          "name" : "from"
        }, {
          "kind" : "string",
          "description" : "Pattern of the result",
          "name" : "to"
        }, {
          "kind" : "number",
          "description" : "Days to add (negative to subtract)",
          "name" : "day"
        }, {
          "kind" : "number",
          "description" : "Weeks to add (negative to subtract)",
          "name" : "week"
        }, {
          "kind" : "number",
          "description" : "Months to add (negative to subtract)",
          "name" : "month"
        }, {
          "kind" : "number",
          "description" : "Years to add (negative to subtract)",
          "name" : "year"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Dates.format(\"31-01-2024\", \"dd-MM-yyyy\", \"yyyy/MM/dd\"))\nprint(Dates.format(\"31-01-2024\", \"dd-MM-yyyy\", \"dd-MM-yyyy\", 0, 0, 1, 0))",
        "runnable" : true
      } ]
    }, {
      "name" : "yearsBetween",
      "description" : "Returns the number of complete years from the first date to the second. The result is negative when the second date is earlier. Both dates must be written as `dd-MM-yyyy`; other formats throw.",
      "returns" : {
        "kind" : "number",
        "description" : "Whole years, negative when the second date is earlier"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "First date as `dd-MM-yyyy`",
          "name" : "date1"
        }, {
          "kind" : "string",
          "description" : "Second date as `dd-MM-yyyy`",
          "name" : "date2"
        } ]
      } ]
    }, {
      "name" : "monthsBetween",
      "description" : "Returns the number of complete months from the first date to the second. The result is negative when the second date is earlier. Both dates must be written as `dd-MM-yyyy`; other formats throw.",
      "returns" : {
        "kind" : "number",
        "description" : "Whole months, negative when the second date is earlier"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "First date as `dd-MM-yyyy`",
          "name" : "date1"
        }, {
          "kind" : "string",
          "description" : "Second date as `dd-MM-yyyy`",
          "name" : "date2"
        } ]
      } ]
    }, {
      "name" : "daysBetween",
      "description" : "Returns the number of complete days from the first date to the second. The result is negative when the second date is earlier. Both dates must be written as `dd-MM-yyyy`; other formats throw.",
      "returns" : {
        "kind" : "number",
        "description" : "Whole days, negative when the second date is earlier"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "First date as `dd-MM-yyyy`",
          "name" : "date1"
        }, {
          "kind" : "string",
          "description" : "Second date as `dd-MM-yyyy`",
          "name" : "date2"
        } ]
      } ]
    }, {
      "name" : "locale",
      "description" : "Sets the language used for day and month names, when parsing and when formatting, for all later calls of this library. Only a plain language code works (`nl`, `en`, `de`, `fr`): a value like `nl-NL` or `nl_NL` is accepted but does not select a language, so names stay in English. An empty value goes back to the language of the agent.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Language code, like `nl` or `en`",
          "name" : "value"
        } ]
      } ],
      "examples" : [ {
        "code" : "Dates.locale(\"nl\")\nprint(Dates.format(\"01-03-2024\", \"dd-MM-yyyy\", \"EEEE d MMMM yyyy\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "with",
      "description" : "Sets parts of a date to fixed values and returns the date in the same pattern. Unlike the shifting arguments of `Dates.format`, the numbers here are the new values (day 1 means the first of the month); 0 keeps the current value. The parts are applied in this order: day of the month, ISO week of the year, month, year. A part that does not exist for the date at that moment (day 31 in a 30-day month, month 13) throws, and a day that is valid when set can still move to the end of the month when the month is changed afterwards.\n\nAll six arguments are required. Throws when the date does not fit the pattern.",
      "returns" : {
        "kind" : "string",
        "description" : "The changed date, written in `pattern`"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Pattern of the date, also used for the result, for example `dd-MM-yyyy`",
          "name" : "pattern"
        }, {
          "kind" : "string",
          "description" : "The date as text",
          "name" : "date"
        }, {
          "kind" : "number",
          "description" : "Day of the month to set (1-31), 0 to keep it",
          "name" : "day"
        }, {
          "kind" : "number",
          "description" : "ISO week of the year to set (1-53), 0 to keep it",
          "name" : "week"
        }, {
          "kind" : "number",
          "description" : "Month to set (1-12), 0 to keep it",
          "name" : "month"
        }, {
          "kind" : "number",
          "description" : "Year to set, 0 to keep it",
          "name" : "year"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Dates.with(\"dd-MM-yyyy\", \"15-03-2024\", 1, 0, 0, 0))   -- first of the month\nprint(Dates.with(\"dd-MM-yyyy\", \"15-03-2024\", 0, 0, 12, 2030))",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Fl",
    "kind" : "library",
    "description" : "Reads, writes, copies, moves, removes, inspects and searches files in the project workspace.\n\nPaths are relative to the workspace of the run, which is the project folder on the agent. Text files are read and written in the JVM default charset (UTF-8 on current Java versions).\n\nWhen a script runs through a Reflex server, a file or folder that is not on the agent yet is first fetched from the project's files on the server, `Fl.remove` also deletes it there, and the files a run leaves behind (except `.lua` files) are copied back to the project when the run ends. A script run directly on the agent (for example with the CLI) only uses its local project folder.\n\nBy default paths are not confined to the workspace: `..` is followed, and so are absolute paths when the script runs directly on the agent. An agent started with `agent.files.confineToWorkspace=true` (`AGENT_FILES_CONFINE_TO_WORKSPACE`) rejects absolute paths and anything that leaves the workspace, symlinks included, in every file function of the libraries.",
    "library" : "Fl",
    "category" : "files",
    "variable" : "Fl",
    "functions" : [ {
      "name" : "mkdir",
      "description" : "Creates a directory including any missing parent directories. Does nothing when it exists.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory to create, relative to the workspace",
          "name" : "folderDir"
        } ]
      } ]
    }, {
      "name" : "getFiles",
      "description" : "Lists the files directly inside a directory, most recently modified first. Subdirectories are not listed and the search is not recursive: use `Fl.find` or `Fl.glob` for that.\n\nEvery entry is the path of the file relative to the workspace, so it starts with the directory you passed (`./report.csv` for `.`, `exports/report.csv` for `exports`). A directory that does not exist gives an empty list.",
      "returns" : {
        "kind" : "table",
        "description" : "List of file paths (strings), newest first"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory to list, relative to the workspace",
          "name" : "location"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"exports/a.txt\", \"a\")\nFl.write(\"exports/b.txt\", \"b\")\nfor _, file in ipairs(Fl.getFiles(\"exports\")) do\n    print(file)\nend",
        "runnable" : true
      } ]
    }, {
      "name" : "beingWritten",
      "description" : "Tells whether a file is still growing: reads its size, waits 500 ms, reads it again and returns `true` when the size changed. Use it to wait until a download or export has finished before reading the file.\n\nBlocks the script for 500 ms and throws when the file does not exist.",
      "returns" : {
        "kind" : "boolean",
        "description" : "true when the size changed during the 500 ms"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File to check, relative to the workspace",
          "name" : "file"
        } ]
      } ]
    }, {
      "name" : "remove",
      "description" : "Deletes a file, or a directory together with everything in it. When the script runs through a Reflex server the file or directory is also deleted from the project's files on the server.\n\nThrows an error when the path does not exist.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory to delete, relative to the workspace",
          "name" : "fileLua"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"scratch/tmp.txt\", \"temporary\")\nFl.remove(\"scratch\")\nprint(Fl.exists(\"scratch\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "copy",
      "description" : "Copies one file. The target is overwritten when it exists and its missing parent directories are created. Directories cannot be copied: the call throws `Failed to copy file`, as it does when the source is missing.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File to copy, relative to the workspace",
          "name" : "source"
        }, {
          "kind" : "string",
          "description" : "Path of the copy, relative to the workspace",
          "name" : "target"
        } ]
      } ]
    }, {
      "name" : "exists",
      "description" : "Checks whether a file or directory exists. When the script runs through a Reflex server, a path that is not on the agent yet is looked up in the project's files on the server and fetched, so project files count as existing.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory, relative to the workspace",
          "name" : "location"
        } ]
      } ]
    }, {
      "name" : "move",
      "description" : "Moves a file or directory to a new path. Missing parent directories of the target are created and an existing target file is replaced. Throws `Source path not found` when the source does not exist. To change only the name, use `Fl.rename`.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory to move, relative to the workspace",
          "name" : "source"
        }, {
          "kind" : "string",
          "description" : "New path, relative to the workspace",
          "name" : "target"
        } ]
      } ]
    }, {
      "name" : "rename",
      "description" : "Renames a file or directory inside its current directory. An existing item with the new name is replaced. Throws when the new name contains a directory part or is absolute, and when the source does not exist. To move something to another directory, use `Fl.move`.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory to rename, relative to the workspace",
          "name" : "source"
        }, {
          "kind" : "string",
          "description" : "New name only, without any directory, like `new.txt`",
          "name" : "newName"
        } ]
      } ]
    }, {
      "name" : "stat",
      "description" : "Returns metadata of a file or directory: type, size in bytes and timestamps. A path that does not exist is not an error: the table then only has `path` and `exists = false`.",
      "returns" : {
        "kind" : "shape",
        "ref" : "FileStat"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory, relative to the workspace",
          "name" : "location"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"report.txt\", \"hello\")\nlocal info = Fl.stat(\"report.txt\")\nprint(info.exists, info.type, info.size, info.modifiedAt)\nprint(Fl.stat(\"missing.txt\").exists)",
        "runnable" : true
      } ]
    }, {
      "name" : "find",
      "description" : "Searches a directory recursively and returns every file and directory found below it (not the directory itself), sorted alphabetically. With a pattern only the matching entries are returned. When `root` is a single file, the result is that file (if it matches). A path that does not exist gives an empty list.\n\nThe returned paths are relative to the workspace (not to `root`) and always use `/`.\n\nThe pattern is a glob such as `*.csv` or `reports/**/*.pdf`, matched against the path relative to `root`. A pattern without `/` is also matched against the bare name of every entry, so `*.csv` finds CSV files at any depth, and a leading `**/` also matches entries directly in `root`. `Fl.glob` does the same with the pattern as the main argument.",
      "returns" : {
        "kind" : "table",
        "description" : "List of paths (strings) relative to the workspace"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory or file to search from, relative to the workspace",
          "name" : "root"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory or file to search from, relative to the workspace",
          "name" : "root"
        }, {
          "kind" : "string",
          "description" : "Glob pattern the entries must match, like `*.csv`",
          "name" : "pattern"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"data/a.csv\", \"1\")\nFl.write(\"data/nested/b.csv\", \"2\")\nFl.write(\"data/c.txt\", \"3\")\nfor _, path in ipairs(Fl.find(\"data\", \"*.csv\")) do\n    print(path)\nend",
        "runnable" : true
      } ]
    }, {
      "name" : "glob",
      "description" : "Returns the files and directories whose path matches a glob pattern, searching the whole workspace or the directory given as first argument. Same as `Fl.find` with a pattern, including how patterns are matched: `*.csv` matches CSV files at any depth. Paths are relative to the workspace, use `/` and are sorted alphabetically.",
      "returns" : {
        "kind" : "table",
        "description" : "List of matching paths (strings) relative to the workspace"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Glob pattern like `*.csv` or `reports/**/*.pdf`",
          "name" : "pattern"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory to search from, relative to the workspace",
          "name" : "root"
        }, {
          "kind" : "string",
          "description" : "Glob pattern like `*.csv` or `reports/**/*.pdf`",
          "name" : "pattern"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"out/one.csv\", \"1\")\nFl.write(\"out/two.csv\", \"2\")\nprint(#Fl.glob(\"*.csv\"))\nprint(#Fl.glob(\"out\", \"one.*\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "read",
      "description" : "Reads a text file and returns its lines as a list. Line breaks are removed, a final line break does not add an empty entry and an empty file gives an empty list. The whole file is read into memory.\n\nThrows `File not found` when the file cannot be read. For structured files use `Json`, `Yaml`, `Csv`, `Xml` or `Xlsx`.",
      "returns" : {
        "kind" : "table",
        "description" : "List of strings, one per line"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File to read, relative to the workspace",
          "name" : "fileLoc"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"notes.txt\", \"first line\\nsecond line\")\nlocal lines = Fl.read(\"notes.txt\")\nprint(#lines, lines[1])\nprint(Fl.exists(\"notes.txt\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "write",
      "description" : "Writes text to a file and creates missing parent directories.\n\nWith two arguments the text is appended to the file, not written over it: when the file already has content a line break is inserted first, so every call adds one line and the file never ends with a trailing line break. To replace the content of the file call it with four arguments and `append = false`.\n\nPass exactly 2 or 4 arguments. With 3 arguments the third is ignored and the call appends. Numbers and other values are converted to text.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File to write, relative to the workspace",
          "name" : "fileName"
        }, {
          "kind" : "string",
          "description" : "Text to write",
          "name" : "content"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "File to write, relative to the workspace",
          "name" : "fileName"
        }, {
          "kind" : "string",
          "description" : "Text to write",
          "name" : "content"
        }, {
          "kind" : "boolean",
          "description" : "true to append to the existing content, false to replace the file",
          "name" : "append"
        }, {
          "kind" : "boolean",
          "description" : "Only used when appending to a file that already has content: true to put a line break before the text",
          "name" : "newLine"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"log.txt\", \"first\")\nFl.write(\"log.txt\", \"second\")          -- appended on a new line\nFl.write(\"log.txt\", \"only line\", false, false) -- replaces the content\nprint(#Fl.read(\"log.txt\"))",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Json",
    "kind" : "library",
    "description" : "Reads, encodes, decodes and converts JSON.\n\n`Json.decode` turns JSON text into Lua tables and `Json.encode` does the reverse. Lua has no null: a JSON `null` becomes `nil`, so object keys with a null value are absent from the decoded table and a null array item leaves a hole in it.\n\nWhen encoding, a table is written as an array if it has a key `1` (or is empty) and as an object otherwise. Object keys must be strings, arrays must not have holes, and objects are written in Lua's table order, which is neither alphabetical nor the order the keys were added.",
    "library" : "Json",
    "category" : "files",
    "variable" : "Json",
    "functions" : [ {
      "name" : "open",
      "description" : "Reads a JSON file and decodes it like `Json.decode`. The file is parsed first with a standard JSON parser, so its parse errors are the ones you see. Throws `Failed to read JSON file` when the file is missing or is not valid JSON. An empty file gives `nil`.",
      "returns" : {
        "kind" : "any",
        "description" : "The decoded value: a table for an object or array, otherwise a string, number or boolean"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JSON file to read, relative to the workspace",
          "name" : "file"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"config.json\", '{\"name\": \"demo\", \"retries\": 3, \"tags\": [\"a\", \"b\"]}')\nlocal config = Json.open(\"config.json\")\nprint(config.name, config.retries, config.tags[2])",
        "runnable" : true
      } ]
    }, {
      "name" : "encode",
      "description" : "Encodes a table as JSON text, pretty-printed with two spaces of indentation. An empty table gives `{}`.\n\nThrows when the argument is not a table, when an object has non-string keys or an array has holes, when a table contains itself, and for values JSON cannot hold (functions, `NaN`, infinity). Write the result to a file with `Fl.write(path, text, false, false)`.",
      "returns" : {
        "kind" : "string",
        "description" : "The JSON text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "table",
          "description" : "Table to encode: an array or an object with string keys",
          "name" : "table"
        } ]
      } ]
    }, {
      "name" : "decode",
      "description" : "Decodes JSON text. Objects and arrays become tables, numbers become Lua numbers and unicode escapes (backslash, u and four hex digits, including surrogate pairs) become UTF-8 text. `null` becomes `nil`.\n\nThrows an error naming the line and column when the text is not valid JSON, including when anything other than whitespace follows the value.",
      "returns" : {
        "kind" : "any",
        "description" : "The decoded value: a table for an object or array, otherwise a string, number or boolean"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JSON text to decode",
          "name" : "json"
        } ]
      } ],
      "examples" : [ {
        "code" : "local data = Json.decode('{\"name\": \"Alice\", \"tags\": [\"a\", \"b\"]}')\nprint(data.name, data.tags[2])\nprint(Json.encode({ answer = 42 }))",
        "runnable" : true
      } ]
    }, {
      "name" : "format",
      "description" : "Re-indents JSON text with two spaces without turning it into a table, so the order of the keys is kept. Throws a parser error when the text is not valid JSON.",
      "returns" : {
        "kind" : "string",
        "description" : "The formatted JSON text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JSON text to format",
          "name" : "js"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Json.format('{\"b\":1,\"a\":[1,2]}'))",
        "runnable" : true
      } ]
    }, {
      "name" : "xml",
      "description" : "Converts XML text to JSON text. Pass the result to `Json.decode` to get a table. Attributes become keys that start with `-`, element text becomes `#text` when the element also has attributes, repeated elements become an array and empty elements get `\"-self-closing\": \"true\"`. All values are strings. Throws when the text is not valid XML. `Json.toXml` converts back.",
      "returns" : {
        "kind" : "string",
        "description" : "The JSON text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "XML text to convert",
          "name" : "xml"
        } ]
      } ],
      "examples" : [ {
        "code" : "local json = Json.xml(\"<order id='7'><item>Widget</item><item>Gadget</item></order>\")\nlocal order = Json.decode(json).order\nprint(order[\"-id\"], #order.item)",
        "runnable" : true
      } ]
    }, {
      "name" : "toXml",
      "description" : "Converts JSON text to XML text with an XML declaration. The single top-level key becomes the root element; with several top-level keys (or an array) a `root` element is added. Array items become repeated elements, keys that start with `-` become attributes and `#text` the text of the element. Numbers, booleans and nulls get an attribute `number=\"true\"`, `boolean=\"true\"` or `null=\"true\"`. Throws `Not a valid JSON string` when the text cannot be parsed.",
      "returns" : {
        "kind" : "string",
        "description" : "The XML text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JSON text to convert",
          "name" : "js"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Json.toXml('{\"order\": {\"-id\": \"7\", \"item\": [\"Widget\", \"Gadget\"]}}'))",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Mail",
    "kind" : "library",
    "description" : "Sends email over SMTP and reads a mailbox over IMAP or POP3. Mail.properties returns a Compose object to send with, Mail.imap and Mail.pop return a Mailbox to read with.\n\nThings to know: the SMTP server is configured with the short keys host, port, auth and starttls or with JavaMail property names ([\"mail.smtp.host\"]); both work. Mail.imap and Mail.pop connect immediately and raise an error when the login fails. Mailboxes are read-only: you can read and search but not delete, move or mark messages. No timeouts are set unless you add the JavaMail timeout properties. Keep passwords in secrets: secret(\"name\"). Close mailboxes with mailbox.close() or Mail.close().",
    "library" : "Mail",
    "category" : "mail",
    "variable" : "Mail",
    "functions" : [ {
      "name" : "properties",
      "description" : "Prepares sending mail over SMTP and returns a Compose object; no connection is made until you call send or sendFile on it. The table is handed to JavaMail as its properties, so JavaMail names work as well. Configure the server with host, port, auth = true to log in with user and password, starttls = true (port 587) or ssl = true (port 465) to encrypt. These are aliases for [\"mail.smtp.host\"], [\"mail.smtp.port\"], [\"mail.smtp.auth\"], [\"mail.smtp.starttls.enable\"] and [\"mail.smtp.ssl.enable\"]; the JavaMail key wins when both are given, and any other mail.smtp.* property can be added, such as [\"mail.smtp.connectiontimeout\"] and [\"mail.smtp.timeout\"] in milliseconds (default: wait forever). The keys user and password (or username) are used as login and the user is also the sender address of every mail. Without a host JavaMail tries localhost:25. An older list form {\"mail.smtp.host, smtp.example.com\"} also works. Raises an error when the argument is not a table.",
      "returns" : {
        "kind" : "object",
        "ref" : "Compose"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "SmtpSettings",
          "description" : "JavaMail properties plus user and password",
          "name" : "list"
        } ]
      } ],
      "examples" : [ {
        "code" : "local smtp = Mail.properties({\n    [\"mail.smtp.host\"] = \"smtp.example.com\",\n    [\"mail.smtp.port\"] = 587,\n    [\"mail.smtp.auth\"] = \"true\",\n    [\"mail.smtp.starttls.enable\"] = \"true\",\n    [\"mail.smtp.connectiontimeout\"] = 10000,\n    [\"mail.smtp.timeout\"] = 20000,\n    user = \"bot@example.com\",\n    password = secret(\"smtp_password\"),\n})\nsmtp.send(\"alice@example.com\", \"Nightly report\", \"<p>The report is ready.</p>\")"
      } ]
    }, {
      "name" : "imap",
      "description" : "Connects to an IMAP server, logs in, opens a folder (INBOX unless folder is given) and returns the Mailbox. SSL is used by default (port 993). Raises an error when host, user or password is missing, when the login or connection fails (\"Failed to connect to mailbox: ...\") or when the folder does not exist. Add JavaMail properties such as [\"mail.imap.connectiontimeout\"] = 10000 to the table to set timeouts, which are unlimited by default. Close the mailbox when you are done.",
      "returns" : {
        "kind" : "object",
        "ref" : "Mailbox"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "MailboxSettings",
          "description" : "Server, login and options; host, user and password are required",
          "name" : "list"
        } ]
      } ],
      "examples" : [ {
        "code" : "local mailbox = Mail.imap({\n    host = \"imap.example.com\",\n    user = \"bot@example.com\",\n    password = secret(\"mail_password\"),\n    [\"mail.imap.connectiontimeout\"] = 10000,\n})\nlocal unread = mailbox.search({ seen = false, subject = \"Invoice\", limit = 10 })\nfor _, message in ipairs(unread) do\n    print(message.subject, message.from[1], message.has_attachments)\nend\nmailbox.close()"
      } ]
    }, {
      "name" : "pop",
      "description" : "Connects to a POP3 server, logs in and returns the Mailbox for its inbox. Same settings and errors as Mail.imap, but without SSL by default (port 110; set ssl = true or port 995 for SSL). POP3 has a single folder (INBOX) and no message flags, so seen is always false and the seen/unseen search criteria are not useful. Prefer IMAP when the server offers it.",
      "returns" : {
        "kind" : "object",
        "ref" : "Mailbox"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "MailboxSettings",
          "description" : "Server, login and options; host, user and password are required",
          "name" : "list"
        } ]
      } ],
      "examples" : [ {
        "code" : "local mailbox = Mail.pop({ host = \"pop.example.com\", user = \"bot@example.com\", password = secret(\"mail_password\"), ssl = true })\nprint(#mailbox.messages() .. \" messages\")\nmailbox.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every mailbox opened with Mail.imap or Mail.pop in this script. Use mailbox.close() to close one.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ],
    "examples" : [ {
      "code" : "-- Send a notification, then wait for the answer in the inbox\nlocal smtp = Mail.properties({\n    [\"mail.smtp.host\"] = \"smtp.example.com\",\n    [\"mail.smtp.port\"] = 587,\n    [\"mail.smtp.auth\"] = \"true\",\n    [\"mail.smtp.starttls.enable\"] = \"true\",\n    user = \"bot@example.com\",\n    password = secret(\"mail_password\"),\n})\nsmtp.send(\"alice@example.com\", \"Please confirm\", \"<p>Reply to confirm the order.</p>\")\n\nlocal mailbox = Mail.imap({ host = \"imap.example.com\", user = \"bot@example.com\", password = secret(\"mail_password\") })\nlocal replies = mailbox.poll({ from = \"alice@example.com\", subject = \"Re: Please confirm\" }, 120)\nAssert.equals(1, #replies)\nmailbox.close()",
      "description" : "Send a mail and wait for the reply"
    } ]
  }, {
    "name" : "Compose",
    "kind" : "object",
    "description" : "Sends email through the SMTP server configured with Mail.properties. Every send opens a new connection, sends one HTML mail from the configured user and closes the connection again. Errors (connection refused, login rejected, invalid address, missing attachment file) are raised as \"Error caught while trying to send e-mail: ...\".",
    "library" : "Mail",
    "category" : "mail",
    "variable" : "smtp",
    "howToGet" : [ {
      "type" : "Mail",
      "function" : "properties",
      "call" : "Mail.properties(list)",
      "returns" : "Compose"
    } ],
    "returnedBy" : [ {
      "type" : "Mail",
      "function" : "properties"
    } ],
    "functions" : [ {
      "name" : "send",
      "description" : "Sends an email from the configured user. The body is sent as HTML (text/html, UTF-8), so use `<br>` or `<p>` for line breaks; plain newlines are collapsed by mail clients. The recipient argument takes one address or several separated by commas (\"a@x.com, b@x.com\"); there is no cc, bcc or reply-to. With logging true the log of the current step is attached as a text file, which is handy for failure notifications. Returns the Compose object when the server accepted the mail, which does not guarantee delivery.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Recipient address, or several separated by commas",
          "name" : "toEmail"
        }, {
          "kind" : "string",
          "description" : "Subject line",
          "name" : "subject"
        }, {
          "kind" : "string",
          "description" : "Message body as HTML",
          "name" : "body"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Recipient address, or several separated by commas",
          "name" : "toEmail"
        }, {
          "kind" : "string",
          "description" : "Subject line",
          "name" : "subject"
        }, {
          "kind" : "string",
          "description" : "Message body as HTML",
          "name" : "body"
        }, {
          "kind" : "boolean",
          "description" : "true to attach the log of the current step; default false",
          "name" : "logging"
        } ]
      } ],
      "examples" : [ {
        "code" : "local smtp = Mail.properties({\n    [\"mail.smtp.host\"] = \"smtp.example.com\",\n    [\"mail.smtp.port\"] = 587,\n    [\"mail.smtp.auth\"] = \"true\",\n    [\"mail.smtp.starttls.enable\"] = \"true\",\n    user = \"bot@example.com\",\n    password = secret(\"mail_password\"),\n})\nsmtp.send(\"alice@example.com, bob@example.com\", \"Nightly run finished\", \"<p>All <b>42</b> checks passed.</p>\")\nsmtp.send(\"qa-team@example.com\", \"Nightly run FAILED\", \"See the attached log.\", true)"
      } ]
    }, {
      "name" : "sendFile",
      "description" : "Sends an HTML email with one file from the project attached. Recipients, body and errors work as in send. The file is attached as it is on disk (a missing file raises an error), under its own file name. With logging true the step log is attached as well.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Recipient address, or several separated by commas",
          "name" : "toEmail"
        }, {
          "kind" : "string",
          "description" : "Subject line",
          "name" : "subject"
        }, {
          "kind" : "string",
          "description" : "Message body as HTML",
          "name" : "body"
        }, {
          "kind" : "string",
          "description" : "Path of the file in the project to attach",
          "name" : "file"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Recipient address, or several separated by commas",
          "name" : "toEmail"
        }, {
          "kind" : "string",
          "description" : "Subject line",
          "name" : "subject"
        }, {
          "kind" : "string",
          "description" : "Message body as HTML",
          "name" : "body"
        }, {
          "kind" : "string",
          "description" : "Path of the file in the project to attach",
          "name" : "file"
        }, {
          "kind" : "boolean",
          "description" : "true to attach the log of the current step; default false",
          "name" : "logging"
        } ]
      } ],
      "examples" : [ {
        "code" : "smtp.sendFile(\"alice@example.com\", \"Report\", \"<p>The report is attached.</p>\", \"reports/summary.pdf\")"
      } ]
    } ]
  }, {
    "name" : "Mailbox",
    "kind" : "object",
    "description" : "A logged-in IMAP or POP3 mailbox with one open folder. List, search and poll messages, and save their attachments to the project. The mailbox is read-only: nothing is deleted, moved or marked as read, so the same messages are found again by the next search. Messages are downloaded when you list or search them, so use search criteria and limit on large folders. The connection is re-established automatically if it was dropped, also after close().",
    "library" : "Mail",
    "category" : "mail",
    "variable" : "mailbox",
    "howToGet" : [ {
      "type" : "Mail",
      "function" : "imap",
      "call" : "Mail.imap(list)",
      "returns" : "Mailbox"
    } ],
    "returnedBy" : [ {
      "type" : "Mail",
      "function" : "imap"
    }, {
      "type" : "Mail",
      "function" : "pop"
    } ],
    "functions" : [ {
      "name" : "folder",
      "description" : "Opens another folder of the mailbox (for example \"Archive\" or \"INBOX/Invoices\"; the separator depends on the server). Raises an error when the folder does not exist, or for any folder other than INBOX on POP3. Later calls read from this folder, and message numbers refer to it.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Folder name as the server knows it, case-sensitive on most servers",
          "name" : "name"
        } ]
      } ]
    }, {
      "name" : "refresh",
      "description" : "Closes and reopens the current folder so that messages that arrived since it was opened become visible. poll does this by itself on every round; use refresh before messages, search or get when you wait for mail some other way.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "messages",
      "description" : "Returns every message in the current folder, newest first. All messages are downloaded and parsed, which is slow for a large folder; use search with a limit instead.",
      "returns" : {
        "kind" : "list",
        "ref" : "MailMessage"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "get",
      "description" : "Returns the message with the given sequence number (the number field of a message, 1 is the oldest). The number can change when messages are added or removed, so use a number from a recent messages, search or poll result. Raises \"No message found with number N\" when it is out of range.",
      "returns" : {
        "kind" : "shape",
        "ref" : "MailMessage"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Sequence number from the number field of a message",
          "name" : "number"
        } ]
      } ]
    }, {
      "name" : "search",
      "description" : "Returns the messages of the current folder that match all given criteria, newest first (see MailSearch for the keys). A plain string instead of a table searches the subject, sender, recipients, body and Message-ID for that text. Matching is done in the agent, case-insensitively, on text that contains the search term. Returns an empty list when nothing matches; it does not wait for new mail (use poll for that) and does not see mail that arrived after the folder was opened until you call refresh. Raises an error for a date in since or before that cannot be parsed.",
      "returns" : {
        "kind" : "list",
        "ref" : "MailMessage"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "MailSearch",
          "description" : "Search criteria table, or a string to search in subject, sender, recipients, body and Message-ID",
          "name" : "criteria"
        } ]
      } ],
      "examples" : [ {
        "code" : "local invoices = mailbox.search({ subject = \"invoice\", hasAttachments = true, since = \"2025-01-01\", limit = 5 })\nfor _, message in ipairs(invoices) do\n    print(message.number, message.subject, message.sent_at_iso)\nend\nlocal byText = mailbox.search(\"password reset\")"
      } ]
    }, {
      "name" : "poll",
      "description" : "Waits for mail: reopens the folder and searches it every interval until at least one message matches, then returns the matches (newest first). When the timeout passes without a match it returns an empty list and does not raise an error, so check the result. A timeout of 0 checks once. Defaults: 30 seconds timeout, 1000 ms interval.\n\nMessages that are already in the folder count too, so a poll for a subject that older mail also has returns immediately. Make the criteria specific (a unique subject or code, or since = <time you sent the request>). With a single number as argument, poll(60) waits up to 60 seconds for any message.",
      "returns" : {
        "kind" : "list",
        "ref" : "MailMessage"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "MailSearch",
          "description" : "Criteria with the same keys as search (a table or string); a number here is read as the timeout in seconds",
          "name" : "criteria"
        } ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "MailSearch",
          "description" : "Criteria with the same keys as search (a table or string); a number here is read as the timeout in seconds",
          "name" : "criteria"
        }, {
          "kind" : "number",
          "description" : "Seconds to keep trying; 0 checks once; default 30",
          "name" : "timeoutSeconds"
        } ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "MailSearch",
          "description" : "Criteria with the same keys as search (a table or string); a number here is read as the timeout in seconds",
          "name" : "criteria"
        }, {
          "kind" : "number",
          "description" : "Seconds to keep trying; 0 checks once; default 30",
          "name" : "timeoutSeconds"
        }, {
          "kind" : "number",
          "description" : "Milliseconds between checks, at least 1; default 1000",
          "name" : "intervalMs"
        } ]
      } ],
      "examples" : [ {
        "code" : "-- trigger the application to send a mail, then wait up to two minutes for it\nlocal messages = mailbox.poll({ subject = \"Your verification code\", to = \"test.user@example.com\" }, 120, 2000)\nAssert.equals(1, #messages)\nlocal code = messages[1].body:match(\"(%d%d%d%d%d%d)\")"
      } ]
    }, {
      "name" : "downloadAttachments",
      "description" : "Saves all attachments of one message into a directory of the project and returns the saved file paths (relative to the project, as a list). The directory is created when needed. File names come from the message with characters that are not allowed in file names (\\ / : * ? \" < > |) replaced by _; when a file with that name exists, -1, -2, ... is added before the extension, so nothing is overwritten. Inline images of the HTML body are not saved. Returns an empty list when the message has no attachments; raises an error when the number is unknown or writing fails.",
      "returns" : {
        "kind" : "table",
        "description" : "List of the saved file paths, relative to the project"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Sequence number from the number field of a message",
          "name" : "messageNumber"
        }, {
          "kind" : "string",
          "description" : "Directory in the project to save the files in",
          "name" : "targetDir"
        } ]
      } ],
      "examples" : [ {
        "code" : "local messages = mailbox.search({ subject = \"Monthly report\", hasAttachments = true, limit = 1 })\nlocal files = mailbox.downloadAttachments(messages[1].number, \"downloads/mail\")\nfor _, path in ipairs(files) do\n    print(path)\nend"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes the open folder and the connection to the server. Errors while closing are ignored. Calling another function afterwards connects again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Ocr",
    "kind" : "library",
    "description" : "Reads text from images and PDF files with the Tesseract OCR engine (LSTM mode), for scanned documents and screenshots. English (eng) and Dutch (nld) are bundled.\n\nThings to know: OCR is slow (seconds per page) and only as good as the scan; a clear, high resolution image works best. The image is converted to black and white before recognition. A call that cannot read the file is retried for about a minute before it fails.",
    "library" : "Ocr",
    "category" : "files",
    "variable" : "Ocr",
    "functions" : [ {
      "name" : "image",
      "description" : "Recognises the text in an image or PDF file of the project (fetched from the Reflex server when it is not in the workspace) and returns it as one string. Images in any format Java can read (PNG, JPEG, GIF, BMP) are handled directly. A file whose name ends in \".pdf\" is rendered page by page at 300 dpi and every page is recognised; the page texts are then joined with a form feed character (\"\\f\"). Use `Ocr.pages` to get a list with one text per page. Pages that contain real text (not scans) are rendered as images too, so prefer a PDF text extractor for those.\n\nThe language is a Tesseract code: \"eng\" (default) or \"nld\"; combinations such as \"eng+nld\" work when every part is installed. An unknown language raises an error that lists the available ones. A non-empty tessdata folder next to the agent replaces the bundled languages, so it must contain the ones you use. Each image or page has a time limit (default 30 seconds, see Ocr.timeout); when it is exceeded an error is raised. Read errors (missing file, not an image) are retried 60 times with a one second pause before the error is raised.",
      "returns" : {
        "kind" : "string",
        "description" : "The recognised text; for a PDF the pages joined with \"\\f\""
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the image or PDF file in the project",
          "name" : "image"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the image or PDF file in the project",
          "name" : "image"
        }, {
          "kind" : "string",
          "description" : "Tesseract language code: \"eng\" (default) or \"nld\"",
          "name" : "lang"
        } ]
      } ],
      "examples" : [ {
        "code" : "local text = Ocr.image(\"scans/invoice.png\", \"nld\")\nlocal invoiceNumber = text:match(\"Factuurnummer:?%s*(%S+)\")\nAssert.notNil(invoiceNumber)"
      } ]
    }, {
      "name" : "pages",
      "description" : "Like `Ocr.image`, but returns a list with one text per page: a PDF gives one entry for every page, an image gives a list with one entry. Language, time limit and error behaviour are the same.",
      "returns" : {
        "kind" : "table",
        "description" : "List of strings, one per page"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the image or PDF file in the project",
          "name" : "image"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the image or PDF file in the project",
          "name" : "image"
        }, {
          "kind" : "string",
          "description" : "Tesseract language code: \"eng\" (default) or \"nld\"",
          "name" : "lang"
        } ]
      } ],
      "examples" : [ {
        "code" : "local pages = Ocr.pages(\"scans/contract.pdf\")\nprint(#pages)"
      } ]
    }, {
      "name" : "timeout",
      "description" : "Sets how long the recognition of one image, or of one PDF page, may take, in seconds, for later Ocr.image and Ocr.pages calls. Default 30. Large scans may need more. Raises an error unless the value is from 1 to 300. Returns nothing, so it cannot be chained.",
      "returns" : {
        "kind" : "nil"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Seconds per image or page, 1 to 300",
          "name" : "sec"
        } ]
      } ],
      "examples" : [ {
        "code" : "Ocr.timeout(120)\nlocal text = Ocr.image(\"scans/large-contract.png\")"
      } ]
    } ]
  }, {
    "name" : "OpenCode",
    "kind" : "library",
    "description" : "Lets a script hand work to the OpenCode CLI (`opencode run`): OpenCode.task runs one instruction, OpenCode.session keeps a conversation going over several messages. Use it to let an AI agent analyse, generate or change files in the project and then check the result in the script.\n\nThings to know: the CLI runs on the machine of the Reflex agent. It must be installed there, on the PATH of the agent process (or set REFLEX_OPENCODE_COMMAND), and have a model provider configured and signed in; Reflex passes no credentials. Check with OpenCode.available(). It runs non-interactively with the project workspace of the current run as its directory (--dir), so it can change files there. Prompts and file contents are processed by the configured AI provider. Arguments are positional (use nil to skip one). A run is killed after 30 minutes unless you give a timeout. A CLI that fails or times out does not raise an error: check result.success (exit is -1 on a timeout). Errors are raised for a missing CLI, a bad argument or an invalid reasoning value.",
    "library" : "OpenCode",
    "category" : "ai",
    "variable" : "OpenCode",
    "functions" : [ {
      "name" : "available",
      "description" : "Returns true when the opencode command can be found on the PATH of the agent process (or at the path in REFLEX_OPENCODE_COMMAND). Does not check that a provider is configured.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "version",
      "description" : "Returns the first line printed by `opencode --version`. Raises an error when the CLI is missing or prints nothing within 10 seconds.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every OpenCode session of this script and stops OpenCode processes that are still running for it. The conversations stay in OpenCode's own history; sessions that were closed cannot send again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "session",
      "description" : "Starts an OpenCode conversation: runs `opencode run --format json <prompt>`, waits for it to finish and returns the session table. Later messages go to the same conversation through session.send (using `--session <id>`), so OpenCode remembers earlier messages. The returned table has the functions send, id and close and the fields sessionId, last (the result of the latest message, see AgentSessionResult) and closed. In a session result, text is all text parts of the answer joined together. Raises an error when the CLI does not report a session id.",
      "returns" : {
        "kind" : "object",
        "ref" : "OpenCodeSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The instruction for the agent, a non-empty string",
          "name" : "prompt"
        }, {
          "kind" : "string",
          "description" : "Model as provider/model, for example anthropic/claude-sonnet-4; passed as -m, nil uses OpenCode's configured default",
          "name" : "model",
          "optional" : true
        }, {
          "kind" : "string",
          "description" : "\"low\", \"medium\" or \"high\" (anything else is an error); nil uses the CLI default",
          "name" : "reasoning",
          "optional" : true
        }, {
          "kind" : "number",
          "description" : "Kill the CLI when it runs longer than this many seconds, must be above 0; default 1800 (30 minutes)",
          "name" : "timeoutSeconds",
          "optional" : true
        } ]
      } ]
    }, {
      "name" : "task",
      "description" : "Runs one instruction through `opencode run` and waits for it to finish. The result is a table, not text: success (exit code 0), exit, text (the answer: the plain output of the CLI with trailing whitespace removed), all (the complete output), and duration (milliseconds). The reasoning argument is passed as --variant. Each task starts with a fresh context; use OpenCode.session for follow-up questions.",
      "returns" : {
        "kind" : "shape",
        "ref" : "AgentTaskResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The instruction for the agent, a non-empty string",
          "name" : "prompt"
        }, {
          "kind" : "string",
          "description" : "Model as provider/model, for example anthropic/claude-sonnet-4; passed as -m, nil uses OpenCode's configured default",
          "name" : "model",
          "optional" : true
        }, {
          "kind" : "string",
          "description" : "\"low\", \"medium\" or \"high\" (anything else is an error); nil uses the CLI default",
          "name" : "reasoning",
          "optional" : true
        }, {
          "kind" : "number",
          "description" : "Kill the CLI when it runs longer than this many seconds, must be above 0; default 1800 (30 minutes)",
          "name" : "timeoutSeconds",
          "optional" : true
        }, {
          "kind" : "boolean",
          "description" : "true to run with --thinking and --print-logs and print the reasoning, logs and answer to the script console while it works; all then contains that readable text instead of the plain output; default false",
          "name" : "showReasoning",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local result = OpenCode.task(\"Read scripts/login.lua and list any step that has no assertion. Reply as a bullet list.\")\nif not result.success then\n    error(\"OpenCode failed (exit \" .. result.exit .. \"): \" .. result.all)\nend\nprint(result.text)",
        "description" : "Ask for a review and fail the script when OpenCode fails"
      }, {
        "code" : "-- pick a model, reasoning variant and a 10 minute limit\nlocal result = OpenCode.task(\"Add a smoke test for the checkout page to tests/checkout.lua\", \"anthropic/claude-sonnet-4\", \"high\", 600)\nprint(result.success, result.duration .. \" ms\")",
        "description" : "Let OpenCode write a file in the project, with a time limit"
      } ]
    } ]
  }, {
    "name" : "OpenCodeSession",
    "kind" : "object",
    "description" : "An OpenCode conversation started by OpenCode.session. The first prompt has already been answered when you get it (see last); send continues the same thread, so OpenCode remembers earlier messages.",
    "library" : "OpenCode",
    "category" : "ai",
    "variable" : "chat",
    "howToGet" : [ {
      "type" : "OpenCode",
      "function" : "session",
      "call" : "OpenCode.session(prompt)",
      "returns" : "OpenCodeSession"
    } ],
    "returnedBy" : [ {
      "type" : "OpenCode",
      "function" : "session"
    } ],
    "functions" : [ {
      "name" : "send",
      "description" : "Sends the next message of the conversation and waits for the answer. Returns the result table, which is also stored as session.last. Raises an error when the session is closed.",
      "returns" : {
        "kind" : "shape",
        "ref" : "AgentSessionResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The next message in the conversation",
          "name" : "prompt"
        } ]
      } ],
      "examples" : [ {
        "code" : "local chat = OpenCode.session(\"Read tests/checkout.lua and remember its structure.\")\nlocal answer = chat.send(\"Which steps are missing assertions?\")\nprint(answer.text)\nchat.close()"
      } ]
    }, {
      "name" : "id",
      "description" : "Returns the OpenCode session id of this conversation, the same value as the sessionId field.",
      "returns" : {
        "kind" : "string",
        "description" : "The session id, nil until the CLI has reported it",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes this session and stops its OpenCode process if one is still running. Afterwards send raises an error.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Play",
    "kind" : "library",
    "description" : "Browser automation with Playwright: starts Chromium and returns a Browser to drive it.\n\n`Play.start()` opens a new browser; every call gives an independent browser. Pages, elements, waits and failure handling are documented on Browser and Locator, including the selector syntax, which differs a little from `Sel`. Playwright only drives Chromium (the Chrome for Testing that the agent downloads), has no Firefox or Docker mode, and waits for elements to be ready before it acts on them. Use `Sel` when you need Firefox or Docker.\n\nWindow size, headless mode and the default timeout come from the project's settings. Downloads go to the project's `.downloads` folder. The run's replay policy controls recording.\n\nClose what you open: `browser.close()` for one browser, `Play.close()` for all of them. After `Play.close()` the library no longer starts browsers.",
    "library" : "Play",
    "category" : "general",
    "variable" : "Play",
    "functions" : [ {
      "name" : "start",
      "description" : "Opens a new Chromium browser and returns it. Fails with an error when the browser cannot be started, or when `Play.close()` has already been called.",
      "returns" : {
        "kind" : "object",
        "ref" : "Browser"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local browser = Play.start()\nbrowser.navigate(\"https://example.com\")\nprint(browser.locator(\"xpath=//h1\").text())\nbrowser.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every browser this library has opened (the same as calling `close()` on each of them). From then on `Play.start()` raises an error and `Play.sleep()` does nothing. Safe to call more than once.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "sleep",
      "description" : "Pauses the script for a number of whole seconds; fractions are cut off, so `Play.sleep(0.5)` does not sleep. Returns at once when the library has been closed. Prefer waiting for a condition (`locator.wait`, `locator.exists`) to a fixed sleep.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Number of seconds to sleep",
          "name" : "seconds"
        } ]
      } ]
    } ]
  }, {
    "name" : "Random",
    "kind" : "library",
    "description" : "Generates random test data: whole numbers, letter strings and IBANs.\n\nValues come from a general purpose random generator and are not suitable for passwords or tokens.",
    "library" : "Random",
    "category" : "strings",
    "variable" : "Random",
    "functions" : [ {
      "name" : "number",
      "description" : "Returns a random whole number. With one argument the range is 0 up to and including `max`; with two arguments it is `min` up to and including `max`. Arguments are cut to whole numbers. Throws when `min` is greater than `max`.",
      "returns" : {
        "kind" : "number",
        "description" : "A whole number inside the range, both ends included"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Highest possible value",
          "name" : "max"
        } ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Lowest possible value",
          "name" : "min"
        }, {
          "kind" : "number",
          "description" : "Highest possible value",
          "name" : "max"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Random.number(1, 100))\nprint(Random.string(8))",
        "runnable" : true
      } ]
    }, {
      "name" : "string",
      "description" : "Returns a random string of exactly `count` letters (a-z and A-Z, no digits or symbols). Throws for a negative count.",
      "returns" : {
        "kind" : "string",
        "description" : "A string of `count` random letters"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Number of letters; 0 gives an empty string",
          "name" : "count"
        } ]
      } ]
    }, {
      "name" : "iban",
      "description" : "Returns a random IBAN with valid check digits and a random account number, written without spaces. The bank code must have the format of the country: four letters for `NL` (`ABNA`), eight digits for `DE`. Throws when the country code is unknown or the bank code does not fit the country.",
      "returns" : {
        "kind" : "string",
        "description" : "An IBAN such as `NL14ABNA4813092681`"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Two-letter country code in capitals, like `NL` or `DE`",
          "name" : "code"
        }, {
          "kind" : "string",
          "description" : "Bank code in the format of that country, like `ABNA` for NL",
          "name" : "bank"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(Random.iban(\"NL\", \"ABNA\"))",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Rest",
    "kind" : "library",
    "description" : "HTTP client for REST and other web APIs. Responses come back as a table with status, body, headers and cookies; there is no response object to close.\n\nThings to know: HTTP error statuses (4xx, 5xx) are returned, not raised, so assert on response.status. Network failures and timeouts raise an error. Redirects are followed. Settings made with bearer(), basic(), timeout(), cookie() and the certificate functions stay active for the rest of the script run and apply to all later requests. TLS certificates are verified unless disableCertificateValidation() is called. Request bodies are text sent as UTF-8; encode tables with Json.encode first. Keep credentials in secrets: secret(\"name\").\n\nHeaders can be given as a map, {[\"Content-Type\"] = \"application/json\"}, which replaces a header of the same name, or as a list of \"Name:value\" strings, {\"X-Trace:abc\", \"X-Trace:def\"}, which adds a header and allows duplicates. List entries without a colon are ignored. In responses, header names are available in their original casing and in lower case, and a header that occurs more than once is a list of values.",
    "library" : "Rest",
    "category" : "api",
    "variable" : "Rest",
    "functions" : [ {
      "name" : "get",
      "description" : "Sends a GET request and returns the response. Redirects are followed automatically. Any HTTP status, including 4xx and 5xx, comes back as a normal response; only network failures and timeouts raise an error (\"Api request failed: ...\").",
      "returns" : {
        "kind" : "shape",
        "ref" : "HttpResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to request",
          "name" : "url"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to request",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic(). Either a map ({Accept = \"application/json\"}) or a list of \"Name:value\" strings; see the Rest library notes",
          "name" : "headers"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.bearer(secret(\"api_token\"))\nlocal response = Rest.get(\"https://api.example.com/users/1\", { Accept = \"application/json\" })\nAssert.equals(200, response.status)\nlocal user = Json.decode(response.body)\nprint(user.name)",
        "description" : "GET with a bearer token and an Accept header"
      } ]
    }, {
      "name" : "post",
      "description" : "Sends a POST request with a text body and returns the response. The body is sent as UTF-8 text and the Content-Type defaults to \"text/plain; charset=UTF-8\", so set the header yourself for JSON or form data. A table is not serialised: encode it first with Json.encode. Non-2xx statuses are returned, not raised.",
      "returns" : {
        "kind" : "shape",
        "ref" : "HttpResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to post to",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "The request body, sent as UTF-8 text",
          "name" : "body"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to post to",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic()",
          "name" : "headers"
        }, {
          "kind" : "string",
          "description" : "The request body, sent as UTF-8 text",
          "name" : "body"
        } ]
      } ],
      "examples" : [ {
        "code" : "local response = Rest.post(\n    \"https://api.example.com/orders\",\n    { [\"Content-Type\"] = \"application/json\", [\"X-Request-Source\"] = \"reflex\" },\n    Json.encode({ sku = \"A-100\", quantity = 2 }))\nAssert.equals(201, response.status)\nprint(Json.decode(response.body).id)"
      } ]
    }, {
      "name" : "put",
      "description" : "Sends a PUT request with a text body and returns the response. Body and Content-Type behave as in post().",
      "returns" : {
        "kind" : "shape",
        "ref" : "HttpResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to send the request to",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "The request body, sent as UTF-8 text",
          "name" : "body"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to send the request to",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic()",
          "name" : "headers"
        }, {
          "kind" : "string",
          "description" : "The request body, sent as UTF-8 text",
          "name" : "body"
        } ]
      } ],
      "examples" : [ {
        "code" : "local response = Rest.put(\n    \"https://api.example.com/users/1\",\n    { [\"Content-Type\"] = \"application/json\" },\n    Json.encode({ name = \"Alice\", active = true }))\nAssert.equals(200, response.status)"
      } ]
    }, {
      "name" : "patch",
      "description" : "Sends a PATCH request with a text body and returns the response. Body and Content-Type behave as in post().",
      "returns" : {
        "kind" : "shape",
        "ref" : "HttpResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to send the request to",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "The request body, sent as UTF-8 text",
          "name" : "body"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to send the request to",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic()",
          "name" : "headers"
        }, {
          "kind" : "string",
          "description" : "The request body, sent as UTF-8 text",
          "name" : "body"
        } ]
      } ],
      "examples" : [ {
        "code" : "local response = Rest.patch(\n    \"https://api.example.com/users/1\",\n    { [\"Content-Type\"] = \"application/merge-patch+json\" },\n    '{\"active\": false}')\nAssert.equals(200, response.status)"
      } ]
    }, {
      "name" : "delete",
      "description" : "Sends a DELETE request and returns the response. The body argument is required: pass \"\" when the request has no body. With two arguments the second one is always the body, so to send headers without a body call delete(url, headers, \"\").",
      "returns" : {
        "kind" : "shape",
        "ref" : "HttpResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to send the request to",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "The request body; use \"\" for none",
          "name" : "body"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to send the request to",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic()",
          "name" : "headers"
        }, {
          "kind" : "string",
          "description" : "The request body; use \"\" for none",
          "name" : "body"
        } ]
      } ],
      "examples" : [ {
        "code" : "local response = Rest.delete(\"https://api.example.com/users/1\", { Accept = \"application/json\" }, \"\")\nAssert.equals(204, response.status)"
      } ]
    }, {
      "name" : "multipart",
      "description" : "Sends a multipart/form-data POST request, for example a file upload. The Content-Type header (including the boundary) is set automatically; do not set it yourself.\n\nEach entry of the parts table becomes one part:\n- name = \"text\": a text part sent as UTF-8 text/plain.\n- {name = \"meta\", value = \"{}\", contentType = \"application/json\"}: a text part with its own content type.\n- {name = \"file\", file = \"docs/report.pdf\", filename = \"r.pdf\", contentType = \"application/pdf\"}: a file part. \"file\" (alias \"path\") is a path in the project; it is fetched from the Reflex server when it is not in the workspace yet. \"filename\" defaults to the file's own name; \"contentType\" is guessed from the extension and falls back to application/octet-stream.\n\nMap entries take their part name from the key, list entries from their \"name\" field. The order of map entries is not guaranteed. Raises an error when the parts are not a table, a part has no name, neither \"value\" nor \"file\" is given, or a file does not exist.",
      "returns" : {
        "kind" : "shape",
        "ref" : "HttpResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to post to",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "The parts: {fieldName = \"text\"} entries and/or {name = ..., value or file = ..., filename = ..., contentType = ...} tables",
          "name" : "parts"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL to post to",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic()",
          "name" : "headers"
        }, {
          "kind" : "table",
          "description" : "The parts: {fieldName = \"text\"} entries and/or {name = ..., value or file = ..., filename = ..., contentType = ...} tables",
          "name" : "parts"
        } ]
      } ],
      "examples" : [ {
        "code" : "local response = Rest.multipart(\n    \"https://api.example.com/documents\",\n    { Authorization = \"Bearer \" .. secret(\"api_token\") },\n    {\n        description = \"Quarterly report\",\n        { name = \"file\", file = \"docs/report.pdf\", contentType = \"application/pdf\" },\n    })\nAssert.equals(201, response.status)"
      } ]
    }, {
      "name" : "download",
      "description" : "Sends a GET request and saves the response body to a file in the project. Returns the response metadata plus the saved path and size; body and response are empty strings because the content is in the file.\n\nIf the target ends with \"/\" or \"\\\" or is an existing directory, the file name is taken from the Content-Disposition header, then from the last segment of the URL, then \"download.bin\". Otherwise the target is the file to write. Missing parent directories are created and an existing file is overwritten. The status is not checked: a 404 page is saved like any other body, so check status before using the file.",
      "returns" : {
        "kind" : "shape",
        "ref" : "DownloadResponse"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL of the file to download",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "Where to save it, relative to the project. A trailing \"/\" or an existing directory means \"save inside this directory\"; anything else is the file path to write",
          "name" : "file"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL of the file to download",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Request headers, applied on top of the defaults set by bearer() and basic()",
          "name" : "headers"
        }, {
          "kind" : "string",
          "description" : "Where to save it, relative to the project. A trailing \"/\" or an existing directory means \"save inside this directory\"; anything else is the file path to write",
          "name" : "file"
        } ]
      } ],
      "examples" : [ {
        "code" : "local response = Rest.download(\n    \"https://example.com/exports/report.csv\",\n    { Authorization = \"Bearer \" .. secret(\"api_token\") },\n    \"downloads/\")\nAssert.equals(200, response.status)\nprint(response.file, response.size)"
      } ]
    }, {
      "name" : "bearer",
      "description" : "Sets a default \"Authorization: Bearer TOKEN\" header that is sent with every later request, to any host, until it is replaced by basic() or removed with clearAuth(). A header with the same name in a request's own headers wins.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The token, without the \"Bearer \" prefix",
          "name" : "token"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.bearer(secret(\"api_token\"))\nlocal response = Rest.get(\"https://api.example.com/me\")"
      } ]
    }, {
      "name" : "basic",
      "description" : "Sets a default \"Authorization: Basic BASE64(username:password)\" header that is sent with every later request, to any host, until it is replaced by bearer() or removed with clearAuth(). A header with the same name in a request's own headers wins.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The user name",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "The password",
          "name" : "password"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.basic(secret(\"api_user\"), secret(\"api_password\"))\nlocal response = Rest.get(\"https://api.example.com/reports\")"
      } ]
    }, {
      "name" : "clearAuth",
      "description" : "Removes the default Authorization header set by bearer() or basic().",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "timeout",
      "description" : "Sets the connect, response and connection-request timeouts to the same value, in milliseconds, for all later requests. The response timeout is the longest the client waits for data on the socket, not the total duration of the request. When a timeout expires the request raises an error. Without a call, connecting and leasing a connection time out after 3 minutes and a response may take forever. Use connectTimeout, responseTimeout or connectionRequestTimeout to set them separately. Raises an error when the value is smaller than 1.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Timeout in milliseconds, at least 1",
          "name" : "millis"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.timeout(10000)\nlocal response = Rest.get(\"https://api.example.com/slow-report\")"
      } ]
    }, {
      "name" : "connectTimeout",
      "description" : "Sets how long to wait while establishing the TCP/TLS connection, in milliseconds. Default: 3 minutes. Raises an error when the value is smaller than 1.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Connect timeout in milliseconds, at least 1",
          "name" : "millis"
        } ]
      } ]
    }, {
      "name" : "responseTimeout",
      "description" : "Sets how long to wait for response data once connected, in milliseconds. It applies to each wait for data, not to the whole transfer. Default: no limit. Raises an error when the value is smaller than 1.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Response timeout in milliseconds, at least 1",
          "name" : "millis"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.responseTimeout(30000)"
      } ]
    }, {
      "name" : "connectionRequestTimeout",
      "description" : "Sets how long to wait for a connection from the client's connection pool, in milliseconds. Every request builds its own client, so this rarely matters. Default: 3 minutes. Raises an error when the value is smaller than 1.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Connection-request timeout in milliseconds, at least 1",
          "name" : "millis"
        } ]
      } ]
    }, {
      "name" : "clearTimeouts",
      "description" : "Removes the timeouts set by timeout() and its variants, restoring the defaults.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "cookies",
      "description" : "Returns the cookies currently in the cookie jar: those received through Set-Cookie headers and those added with cookie(). The jar lives as long as the Rest library (one script run) and is sent automatically with later requests to matching hosts.",
      "returns" : {
        "kind" : "list",
        "ref" : "RestCookie"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "Rest.post(\n    \"https://app.example.com/login\",\n    { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" },\n    \"user=alice&password=\" .. secret(\"app_password\"))\nfor _, c in ipairs(Rest.cookies()) do\n    print(c.name, c.domain, c.secure)\nend"
      } ]
    }, {
      "name" : "cookie",
      "description" : "Adds a cookie to the cookie jar for the host of the given URL (the URL's path is ignored). The cookie is marked secure when the URL uses https. Raises an error when the URL has no host.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "A URL on the host that should receive the cookie",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "Cookie name",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Cookie value",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "A URL on the host that should receive the cookie",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "Cookie name",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Cookie value",
          "name" : "value"
        }, {
          "kind" : "string",
          "description" : "Path the cookie applies to; \"/\" when left out",
          "name" : "path"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.cookie(\"https://app.example.com\", \"session\", secret(\"session_id\"))\nlocal response = Rest.get(\"https://app.example.com/dashboard\")"
      } ]
    }, {
      "name" : "clearCookies",
      "description" : "Empties the cookie jar, for example to start a new login session.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "disableCertificateValidation",
      "description" : "Turns off TLS certificate and host name verification for all later requests, to any host, for the rest of the run. Meant for test environments with self-signed certificates; it cannot be switched back on. Leave it out when the server has a valid certificate.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "Rest.disableCertificateValidation()\nlocal response = Rest.get(\"https://test.internal.example:8443/health\")"
      } ]
    }, {
      "name" : "clientCertificate",
      "description" : "Registers a PKCS#12 (.p12/.pfx) client certificate for mutual TLS. It is presented only on requests to the given host and port (a URL without a port means 443 for https and 80 for http); calling it again for the same host and port replaces the certificate. The file is read from the project (fetched from the Reflex server when it is not in the workspace). A copy fetched from the server is removed from the workspace after loading; a file that was already in the workspace is left alone. Raises \"Failed to configure client certificate: ...\" when the port is outside 1-65535, the file is missing, or the passphrase is wrong. Use secret() for the passphrase.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Host name of the HTTPS endpoint, without scheme or port",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "Port of the endpoint, 1-65535",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "PKCS#12 file in the project",
          "name" : "file"
        }, {
          "kind" : "string",
          "description" : "Passphrase of the file; nil is treated as an empty one",
          "name" : "passphrase"
        } ]
      } ],
      "examples" : [ {
        "code" : "Rest.clientCertificate(\"api.partner.example\", 443, \"certs/client.p12\", secret(\"client_cert_passphrase\"))\nlocal response = Rest.get(\"https://api.partner.example/orders\")"
      } ]
    }, {
      "name" : "cert",
      "description" : "Alias for clientCertificate(host, port, file, passphrase).",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Host name of the HTTPS endpoint, without scheme or port",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "Port of the endpoint, 1-65535",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "PKCS#12 file in the project",
          "name" : "file"
        }, {
          "kind" : "string",
          "description" : "Passphrase of the file; nil is treated as an empty one",
          "name" : "passphrase"
        } ]
      } ]
    } ],
    "examples" : [ {
      "code" : "-- Log in, keep the session cookie, call an endpoint\nRest.timeout(15000)\nlocal login = Rest.post(\n    \"https://app.example.com/api/login\",\n    { [\"Content-Type\"] = \"application/json\" },\n    Json.encode({ user = \"alice\", password = secret(\"app_password\") }))\nAssert.equals(200, login.status)\n\nlocal response = Rest.get(\"https://app.example.com/api/orders?status=open\", { Accept = \"application/json\" })\nAssert.equals(200, response.status)\nprint(response.headers[\"content-type\"])\nlocal orders = Json.decode(response.body)",
      "description" : "Session login followed by an authenticated call"
    } ]
  }, {
    "name" : "Retry",
    "kind" : "library",
    "description" : "Runs a Lua function again when it fails, for steps that can fail temporarily (an API that is not ready yet, a file that has not arrived). Combine it with `Assert` to retry until a condition holds.\n\nFor browser steps use the retry behaviour of the browser libraries instead.",
    "library" : "Retry",
    "category" : "utils",
    "variable" : "Retry",
    "functions" : [ {
      "name" : "run",
      "description" : "Calls the function up to `attempts` times until it finishes without an error. Any error raised by the function (including a failed `Assert`) counts as a failed attempt. Optionally waits `delayMs` between attempts and stops retrying once `maxDurationMs` has passed since the first attempt; the limit is only checked between attempts, so a slow attempt can overrun it.\n\nWhen the last attempt also fails, the error of that last attempt is thrown unchanged. Stopping the run aborts immediately without further attempts. The function is called without arguments.\n\nUse exactly 2, 3 or 4 arguments, with the function always last: `(attempts, fn)`, `(attempts, delayMs, fn)` or `(attempts, delayMs, maxDurationMs, fn)`. Invalid values (attempts below 1, a negative delay, a max duration below 1, a last argument that is not a function) throw before anything runs.",
      "returns" : {
        "kind" : "any",
        "description" : "The first value returned by the function on the successful attempt, or nil when it returned nothing"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum number of attempts, including the first run (at least 1)",
          "name" : "attempts"
        }, {
          "kind" : "function",
          "description" : "The function to run; called without arguments",
          "name" : "function"
        } ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum number of attempts, including the first run (at least 1)",
          "name" : "attempts"
        }, {
          "kind" : "number",
          "description" : "Wait between attempts in milliseconds (0 or more)",
          "name" : "delayMs"
        }, {
          "kind" : "function",
          "description" : "The function to run; called without arguments",
          "name" : "function"
        } ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum number of attempts, including the first run (at least 1)",
          "name" : "attempts"
        }, {
          "kind" : "number",
          "description" : "Wait between attempts in milliseconds (0 or more)",
          "name" : "delayMs"
        }, {
          "kind" : "number",
          "description" : "Stop retrying after this many milliseconds in total (at least 1)",
          "name" : "maxDurationMs"
        }, {
          "kind" : "function",
          "description" : "The function to run; called without arguments",
          "name" : "function"
        } ]
      } ],
      "examples" : [ {
        "code" : "local attempts = 0\nlocal value = Retry.run(3, 10, function()\n    attempts = attempts + 1\n    Assert.that(attempts >= 2, \"not ready yet\")\n    return \"done after \" .. attempts\nend)\nprint(value)",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Samba",
    "kind" : "library",
    "description" : "Reads and writes files on Windows/Samba shares (SMB 2 and 3; servers that only offer SMB 1 do not work). Samba.newSession returns an SambaSession for one share.\n\nThings to know: there is no persistent connection. Every operation connects, logs in, does its work and disconnects again, so errors (unreachable host, wrong password, unknown share) appear at the first operation, not in newSession. There is no domain, port or timeout setting. Use secret() for the password.",
    "library" : "Samba",
    "category" : "files",
    "variable" : "Samba",
    "functions" : [ {
      "name" : "newSession",
      "description" : "Creates a session for one share (for example \"reports\" in \\\\fileserver\\reports). Only stores the settings; it does not connect. The user name is sent with an empty domain.",
      "returns" : {
        "kind" : "object",
        "ref" : "SambaSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Host name or IP address of the file server",
          "name" : "hostname"
        }, {
          "kind" : "string",
          "description" : "User name to log in with",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret()",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Name of the share, without the server or slashes",
          "name" : "shareName"
        } ]
      } ],
      "examples" : [ {
        "code" : "local share = Samba.newSession(\"fileserver.example.com\", \"reports-bot\", secret(\"samba_password\"), \"reports\")\nshare.copyRemoteToLocal(\"2025/summary.xlsx\", \"input/summary.xlsx\")\nshare.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every Samba session created with Samba.newSession in this script.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "SambaSession",
    "kind" : "object",
    "description" : "One Samba/SMB share. Paths are relative to the root of the share. Each operation opens and closes its own connection, so the session can be used for a long time without keeping a connection alive.",
    "library" : "Samba",
    "category" : "files",
    "variable" : "share",
    "howToGet" : [ {
      "type" : "Samba",
      "function" : "newSession",
      "call" : "Samba.newSession(hostname, username, password, shareName)",
      "returns" : "SambaSession"
    } ],
    "returnedBy" : [ {
      "type" : "Samba",
      "function" : "newSession"
    } ],
    "functions" : [ {
      "name" : "listFiles",
      "description" : "Lists the names of the entries in a directory of the share that match a search pattern. Files and subdirectories are both returned, as names only. SMB servers usually include the special entries \".\" and \"..\". Use \"\" as path for the share root and \"*\" as pattern for everything.",
      "returns" : {
        "kind" : "table",
        "description" : "List of entry names"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory in the share; \"\" for the root",
          "name" : "path"
        }, {
          "kind" : "string",
          "description" : "Wildcard pattern, for example \"*.xlsx\" or \"*\"",
          "name" : "searchPattern"
        } ]
      } ],
      "examples" : [ {
        "code" : "for _, name in ipairs(share.listFiles(\"2025\", \"*.xlsx\")) do\n    print(name)\nend"
      } ]
    }, {
      "name" : "copyRemoteToLocal",
      "description" : "Downloads a file from the share into the project. Missing local folders are created and an existing local file is overwritten. Raises \"Failed to copy remote file to local file: ...\" on failure.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file in the share",
          "name" : "remoteFile"
        }, {
          "kind" : "string",
          "description" : "Path in the project to save the file as",
          "name" : "localFile"
        } ]
      } ],
      "examples" : [ {
        "code" : "share.copyRemoteToLocal(\"2025/summary.xlsx\", \"input/summary.xlsx\")"
      } ]
    }, {
      "name" : "copyLocalToRemote",
      "description" : "Uploads a project file (fetched from the Reflex server first when it is not in the workspace) to the share. Missing folders on the share are created. When the remote file exists it is replaced only if overwrite is true; otherwise the copy fails.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file in the project",
          "name" : "localFile"
        }, {
          "kind" : "string",
          "description" : "Path in the share to write the file to",
          "name" : "remoteFile"
        }, {
          "kind" : "boolean",
          "description" : "true to replace an existing remote file; default false",
          "name" : "overwrite",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "share.copyLocalToRemote(\"output/report.pdf\", \"2025/05/report.pdf\", true)"
      } ]
    }, {
      "name" : "removeFile",
      "description" : "Deletes a file in the share. Raises an error when it cannot be deleted.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file in the share",
          "name" : "remoteFile"
        } ]
      } ]
    }, {
      "name" : "close",
      "description" : "Releases the SMB client of this session. Operations already close their own connection, so this is only housekeeping; do not use the session afterwards.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Sel",
    "kind" : "library",
    "description" : "Browser automation with Selenium: starts Chrome (default) or Firefox and returns a Browser to drive it.\n\n`Sel.start()` opens a new browser; every call gives an independent browser with its own session. Pages, elements, waits and failure handling are documented on Browser and Locator, including the selector syntax. For a Playwright based alternative that drives Chromium only, see `Play`.\n\nWindow size, headless mode and the default timeout come from the project's settings. Chrome is the Chrome for Testing that the agent downloads. When the agent runs with `agent.selenium.docker=true`, the browser runs in a `selenium/standalone-chrome` or `selenium/standalone-firefox` Docker container instead (headless does not apply there); files the browser downloads are copied to the project's `.downloads` folder. The run's replay policy controls recording. Downloads in a local Chrome also go to `.downloads`.\n\nClose what you open: `browser.close()` for one browser, `Sel.close()` for all of them. After `Sel.close()` the library no longer starts browsers.",
    "library" : "Sel",
    "category" : "general",
    "variable" : "Sel",
    "functions" : [ {
      "name" : "start",
      "description" : "Opens a new browser and returns it. Chrome is the default; pass `true` for Firefox, which Selenium starts itself (the agent does not download it as it does for Chrome; in Docker mode the image provides it). Fails with an error when the browser cannot be started. After `Sel.close()` it does not start a browser but returns the Sel library itself, so only use the result for a browser you know is open.",
      "returns" : {
        "kind" : "object",
        "ref" : "Browser"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "boolean",
          "description" : "`true` starts Firefox instead of Chrome. Default `false`",
          "name" : "firefox"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nprint(browser.locator(\"xpath=//h1\").text())\nbrowser.close()"
      }, {
        "code" : "local browser = Sel.start(true) -- Firefox\nbrowser.navigate(\"https://example.com\")\nprint(browser.url())\nbrowser.close()",
        "description" : "Start Firefox instead of Chrome"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every browser this library has opened (the same as calling `close()` on each of them) and stops the Docker container in Docker mode, copying its downloads to `.downloads`. From then on `Sel.start()` raises an error and `Sel.sleep()` returns at once. Safe to call more than once.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "sleep",
      "description" : "Pauses the script for a number of whole seconds; fractions are cut off, so `Sel.sleep(0.5)` does not sleep. It sleeps in steps of one second and returns early once the library has been closed, for example when the run is stopped. Prefer waiting for a condition (`locator.wait`, `locator.exists`) to a fixed sleep.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Number of seconds to sleep",
          "name" : "seconds"
        } ]
      } ]
    } ]
  }, {
    "name" : "Browser",
    "kind" : "object",
    "description" : "An open browser window with its tabs, driven by Selenium (`Sel.start()`) or Playwright (`Play.start()`). Both engines give you the same functions; where they behave differently, the function says so.\n\nPage-level functions (`navigate`, `back`, `switchToNextTab`, ...) return the browser itself, so calls can be chained. Work on elements goes through `locator(selector)`, which returns a Locator.\n\n**Retries and timeouts.** Every browser action is retried every 100 ms until it succeeds or the timeout has passed. The timeout is the project's Selenium timeout setting in milliseconds (10000 for new projects); `setTimeOut` changes it for this browser. On Selenium only \"transient\" errors are retried: element not found, click intercepted, element not interactable, stale element, missing frame, window or alert, an unexpected alert, a JavaScript error, and Lua errors such as an unresolved `?Label` selector. Anything else, such as a failed page load, fails at once. On Playwright every error is retried. The same timeout applies to Playwright operations and navigation.\n\n**When an action still fails.** By default (`stopOnFailure(true)`) the script stops with an error such as `[click: id=save] Stopped with exception -> no such element ...`. The parameters in the message have secrets masked. A screenshot and the page source are saved with the run's error details, and the browser is closed (it stays open when the run is started with `--debug on-error` or `--debug always`, so you can inspect it). The error can be caught with `pcall`. After `stopOnFailure(false)` no error is raised: locator actions return a result table whose second entry is `false`, and `text`/`attribute`/`property`/`tag` return `nil`. (`exists`, `visible`, `enabled` and `selected` never raise; they return `false` when the element is missing.) Page-level functions such as `navigate` return the browser either way, so a failure there cannot be seen from the return value.\n\nClose the browser with `close()` when you are done; `Sel.close()` / `Play.close()` close every browser the library opened.",
    "library" : "Sel",
    "category" : "general",
    "variable" : "browser",
    "initialize" : "Sel.start()",
    "howToGet" : [ {
      "type" : "Sel",
      "function" : "start",
      "call" : "Sel.start()",
      "returns" : "Browser"
    } ],
    "returnedBy" : [ {
      "type" : "Play",
      "function" : "start"
    }, {
      "type" : "Sel",
      "function" : "start"
    } ],
    "functions" : [ {
      "name" : "navigate",
      "description" : "Loads a URL in the current tab and waits for the page's load event. The URL is used as given, so include the scheme (`https://`). Selenium waits with the WebDriver's default page load timeout; Playwright gives up after the project timeout. A failed load is retried until the timeout (Playwright) or fails at once (Selenium).",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Absolute URL including the scheme, for example `https://example.com/login`",
          "name" : "url"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nprint(browser.url())\nbrowser.close()"
      } ]
    }, {
      "name" : "file",
      "description" : "Opens an HTML file from the project in the current tab, as a `file://` URL. Use it for local test pages and generated reports. If the file is not in the run's workspace yet, it is fetched from the project on the server first.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the HTML file, relative to the project directory",
          "name" : "file"
        } ]
      } ]
    }, {
      "name" : "back",
      "description" : "Goes back one entry in the history of the current tab.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "forward",
      "description" : "Goes forward one entry in the history of the current tab.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "refresh",
      "description" : "Reloads the current page.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "url",
      "description" : "Returns the URL of the current tab, after any redirects. This is a plain read: it is not retried, and a browser that has gone away raises an error.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "source",
      "description" : "Returns the HTML of the current page as the browser holds it right now: the live DOM including changes made by scripts, not the response the server sent. The content of shadow roots is not included. On Selenium this is the frame the last element lookup ended in, which is an iframe after an element was found inside one. This is a plain read and is not retried.",
      "returns" : {
        "kind" : "string"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "executeJs",
      "description" : "Runs JavaScript in the current page and returns the result converted to a string, or nil when the script returns nothing.\n\nSelenium runs the script as a function body, so write `return document.title` to get a value. Playwright first evaluates the script as an expression (`document.title`) and, when that throws, runs it again as a function body. `return ...` therefore works on both engines, but on Playwright a script that fails at run time executes twice.\n\nNumbers and booleans come back as strings (`\"42\"`, `\"true\"`). Objects and arrays are not converted to JSON but to their Java text form, so return `JSON.stringify(value)` and decode it with `Json.decode` for structured data. A script that throws is retried until the timeout, so side effects can be repeated. To run a script against one element, use `locator.execute`.",
      "returns" : {
        "kind" : "string",
        "description" : "The script's result as a string, nil when it returned nothing",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JavaScript to run, with `return` for the value you want",
          "name" : "script"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nlocal title = browser.executeJs(\"return document.title\")\nprint(title)\nbrowser.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes this browser with all its tabs. Calling it again, or on a browser that was already closed because an action failed, does nothing.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "closed",
      "description" : "Returns true once this browser has been closed, either by `close()` or because an action failed and stopped the script.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "locator",
      "description" : "Creates a Locator for a selector. Nothing is looked up yet: the element is searched again every time you call a function on the locator. See Locator for the selector syntax, shadow DOM, iframes and `?Label` selectors from `locators.yaml`.",
      "returns" : {
        "kind" : "object",
        "ref" : "Locator"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Selector such as `id=username`, `css=.menu a` or `xpath=//button`, or a `?Label` from locators.yaml",
          "name" : "selector"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/login\")\nbrowser.locator(\"id=username\").fill(\"alice\")\nbrowser.locator(\"css=button[type=submit]\").click()\nprint(browser.locator(\"css=h1\").text())\nbrowser.close()"
      } ]
    }, {
      "name" : "l",
      "description" : "Short alias for `locator`.",
      "returns" : {
        "kind" : "object",
        "ref" : "Locator"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Selector, see `locator`",
          "name" : "selector"
        } ]
      } ]
    }, {
      "name" : "cke",
      "description" : "Replaces the content of a CKEditor 4 editor with HTML. It finds the first iframe with the class `cke_wysiwyg_frame`, sets the inner HTML of its `.cke_editable` body and scrolls the editor into view.\n\nReturns true on success; failures honour `stopOnFailure`.",
      "returns" : {
        "kind" : "boolean",
        "description" : "True on success, false after a failure when stopOnFailure is off"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "HTML to put in the editor, for example `<p>Hello</p>`",
          "name" : "val"
        } ]
      } ]
    }, {
      "name" : "acceptAlert",
      "description" : "Accepts (clicks OK on) an open JavaScript alert, confirm or prompt dialog. Nothing happens when no dialog is open. You only need this after `autoAcceptAlerts(false)`; by default dialogs are accepted automatically.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "switchToNextTab",
      "description" : "Makes the next open tab the current one, wrapping around from the last tab to the first. Does nothing when only one tab is open. Later actions run in the current tab. Playwright switches to a tab opened by the page (a popup or `target=_blank` link) by itself; Selenium stays on the old tab until you switch.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "switchToPreviousTab",
      "description" : "Makes the previous tab the current one, wrapping around from the first tab to the last. Does nothing when only one tab is open.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "closeTab",
      "description" : "Closes the current tab and makes the tab before it current (the next one when the closed tab was the first). Does nothing when it is the only tab; use `close()` to end the browser.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "stopOnFailure",
      "description" : "Decides what happens when an action still fails after its retries. With `true` (the default) the script stops with an error, a screenshot and the page source are saved and the browser is closed. With `false` no error is raised and the script continues: locator actions return a result table whose second entry is `false`, and `text`/`attribute`/`property`/`tag` return nil. The setting stays in effect for this browser until you change it again. Calling it without an argument is the same as `stopOnFailure(true)`.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "boolean",
          "description" : "`true` stops the script on a failed action, `false` lets it continue",
          "name" : "stop"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nbrowser.stopOnFailure(false)\nlocal result = browser.locator(\"id=cookie-banner-close\").click()\nif not result[2] then\n  print(\"no cookie banner, continuing\")\nend\nbrowser.stopOnFailure(true)\nbrowser.close()"
      } ]
    }, {
      "name" : "autoAcceptAlerts",
      "description" : "Switches automatic handling of JavaScript alert, confirm and prompt dialogs. When on (the default), an open dialog is accepted by itself: Selenium accepts it before each action, Playwright as soon as it opens. When off, the dialog stays open until you call `acceptAlert()`; on Selenium every action in between fails with an unexpected-alert error, on Playwright the page is blocked. Calling it without an argument is the same as `autoAcceptAlerts(true)`.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "boolean",
          "description" : "`true` accepts dialogs automatically, `false` leaves them open",
          "name" : "enabled"
        } ]
      } ]
    }, {
      "name" : "setTimeOut",
      "description" : "Sets how long actions of this browser are retried before they count as failed, in milliseconds. This is the default for locator waits (`exists`, `visible`, `wait`, ...) too. Without an argument it goes back to the project's timeout setting. Also updates Playwright's per-operation and navigation timeouts.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Timeout in milliseconds",
          "name" : "milliseconds"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.setTimeOut(30000) -- slow page: retry for up to 30 seconds\nbrowser.navigate(\"https://example.com/report\")\nbrowser.locator(\"id=result\").wait()\nbrowser.setTimeOut() -- back to the project default\nbrowser.close()"
      } ]
    } ]
  }, {
    "name" : "Locator",
    "kind" : "object",
    "description" : "A handle on one element, or a group of elements, on the page, found by a selector. Create one with `browser.locator(selector)`.\n\nA locator does not touch the page when it is created. Every function called on it searches the page again, so a locator keeps working when the page re-renders, and one locator can be used for several actions. Actions use the first element that matches; `list` returns all of them.\n\n**Selenium selectors (`Sel`)**\n\n- `xpath=//button[@type='submit']`, `css=form .primary`, `id=username`, `name=email`, `class=btn` (one class name only), `tag=h1` and `linkText=Sign in` (exact link text).\n- A bare selector is XPath only when it starts with `/`, `./` or `(`; otherwise it is CSS.\n- Semantic selectors: `role=button[name=\"Save\"]`, `text=Save`, `label=Email`, `placeholder=Email`, `alt=Logo`, `title=Help`, `testid=save` (also `testId=`). Quoted text is exact; unquoted text matches a case-insensitive substring.\n\n**Playwright selectors (`Play`)**\n\n- `xpath=`, `css=`, `id=`, `name=`, `class=`, `tag=` and `linkText=` work as in Selenium (`class=` accepts several class names). Playwright's own `text=Log in` (case-insensitive substring; `text=\"Log in\"` for an exact match) and `role=button[name=\"Save\"]` work too.\n- A selector without prefix is XPath when it starts with `/`, `./` or `(`, and CSS otherwise. So `button` is CSS here.\n- `label=`, `placeholder=`, `alt=`, `title=` and `testId=` use the corresponding Playwright getBy functions.\n\n**Chains.** `css=form >> css=input` finds the input inside the form: each step is a selector of its own (any prefix) that is searched inside the elements the previous step found. `>>` inside quotes is left alone.\n\n**Shadow DOM.** `css=my-app >>> css=button` is short for `css=my-app >> shadow >> css=button`: the `shadow` step enters the host's open shadow root (closed shadow roots cannot be entered). On Selenium, a selector without a chain that is not found in the page is also searched in the open shadow roots of every element on the page (nested ones included), and elements found in a shadow root are clicked with JavaScript (`element.click()`), which skips the visibility and overlay checks of a real click. Playwright pierces open shadow roots by itself for `css=` and `text=` selectors; `>>>` is turned into `>>`. XPath does not pierce shadow roots on Playwright.\n\n**Iframes.** There is no frame to switch to: a selector that is not found in the main page is searched in every iframe, nested ones included, and the first match wins. On Selenium the browser stays inside that frame until the next lookup, which matters for `browser.executeJs` and `browser.source`.\n\n**`?Label` selectors.** A selector that starts with `?` is looked up in the project's `locators.yaml` instead of being used directly:\n\n```yaml\nshorts:                       # which page you are on\n  - url: \"example.com\"        # part of the current URL (plain substring, not a pattern)\n    id: \"example\"\nactions:\n  - action: \"fill\"            # the action: click, fill, type, radio, select, text, ...\n    id: [\"example\"]           # the shorts that apply\n    locators:                 # tried in this order, first one that exists wins\n      - \"xpath=//label[normalize-space()='${place}']/following::input[1]\"\n      - \"css=input[placeholder='${place}']\"\n```\n\n`${place}` is the text after the `?`, so `browser.locator(\"?Email\")` gives `Email`. `${label}` is the value the action works with (the text for `fill` and `type`, the option for `select`, the value for `radio`, the file for `send`, the script for `execute`); it is only available for actions that take a value. The action names are the function names, except `execute` (`evaluate`), `enter` (`buttonEnter`) and `tab` (`buttonTab`). Resolution is retried for each action, including `wait`. If `locators.yaml` is missing it is fetched from the project on the server; if no candidate matches, the error lists the selectors that were tried.\n\n**Results and failures.** Actions (`click`, `fill`, `type`, `select`, ...) return a table `{locator, succeeded}`: `result[1]` is this locator and `result[2]` is `true`. They are retried until the browser's timeout; when they still fail, the script stops with an error unless `browser.stopOnFailure(false)` was called, in which case `result[2]` is `false`. Reading functions (`text`, `attribute`, `property`, `tag`) return the value, or nil after a failure that did not stop the script. The wait-style checks (`exists`, `visible`, `enabled`, `selected`) return a boolean and never raise an error.",
    "library" : "Sel",
    "category" : "general",
    "variable" : "locator",
    "howToGet" : [ {
      "type" : "Sel",
      "function" : "start",
      "call" : "Sel.start()",
      "returns" : "Browser"
    }, {
      "type" : "Browser",
      "function" : "locator",
      "call" : "browser.locator(selector)",
      "returns" : "Locator"
    } ],
    "returnedBy" : [ {
      "type" : "Browser",
      "function" : "locator"
    }, {
      "type" : "Browser",
      "function" : "l"
    }, {
      "type" : "Locator",
      "function" : "list"
    }, {
      "type" : "Locator",
      "function" : "locator"
    } ],
    "functions" : [ {
      "name" : "click",
      "description" : "Clicks the element. Selenium performs a real click on the element; when something covers it, the click is retried until the overlay is gone or the timeout passes. Playwright waits until the element is visible, stable, enabled and not covered. Elements found inside a shadow root are clicked with JavaScript on Selenium.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/login\")\nbrowser.locator(\"id=username\").fill(\"alice\")\nbrowser.locator(\"css=button[type=submit]\").click()\nbrowser.close()"
      } ]
    }, {
      "name" : "radio",
      "description" : "Selects the radio input with the given value. The selector can match that input or its container. An already selected input stays selected.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Value attribute of the radio input; also substitutes `${label}` in a `?Label` selector",
          "name" : "value"
        } ]
      } ],
      "examples" : [ {
        "code" : "-- Playwright: the selector matches the group, the value picks the option\nlocal browser = Play.start()\nbrowser.navigate(\"https://example.com/survey\")\nbrowser.locator(\"id=member\").radio(\"yes\")\nbrowser.close()"
      } ]
    }, {
      "name" : "check",
      "description" : "Ticks a checkbox or radio input if it is not selected yet. Use `click` to untick a checkbox.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "fill",
      "description" : "Replaces the content of an input or textarea with `value`. With events on (the default), the field is clicked first, and after typing Tab is pressed and the field is clicked again if it is still enabled and visible, so that change and blur handlers run. With events off, only the content is replaced. Use `type` to append to what is already there.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The text to put in the field",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The text to put in the field",
          "name" : "value"
        }, {
          "kind" : "boolean",
          "description" : "`true` (default) clicks the field before and presses Tab after typing; `false` only replaces the content. An explicit nil counts as false",
          "name" : "fire"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/login\")\nbrowser.locator(\"id=username\").fill(\"alice\")\nbrowser.locator(\"id=password\").fill(secret(\"password\"))\nbrowser.close()"
      } ]
    }, {
      "name" : "type",
      "description" : "Types `value` into an input key by key, after whatever is already in the field. With events on (the default), the field is clicked first, and after typing Tab is pressed and the field is clicked again if it is still enabled and visible. Use `fill` to replace the content instead.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The text to type into the field",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The text to type into the field",
          "name" : "value"
        }, {
          "kind" : "boolean",
          "description" : "`true` (default) clicks the field before and presses Tab after typing; `false` only types. An explicit nil counts as false",
          "name" : "fire"
        } ]
      } ]
    }, {
      "name" : "send",
      "description" : "Attaches a file from the project to a file input (`<input type=\"file\">`), as if the user had chosen it. If the file is not in the run's workspace yet it is fetched from the project on the server first. Nothing is submitted; click the form's button afterwards.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file, relative to the project directory",
          "name" : "value"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/upload\")\nbrowser.locator(\"css=input[type=file]\").send(\"files/report.pdf\")\nbrowser.locator(\"id=upload\").click()\nbrowser.close()"
      } ]
    }, {
      "name" : "select",
      "description" : "Picks an option in a `<select>` dropdown by the text the user sees. The text must match the option exactly. On Selenium the element must be a `<select>`; it fails with an error that is not retried otherwise.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Visible text of the option, exact match",
          "name" : "value"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/form\")\nbrowser.locator(\"id=country\").select(\"Netherlands\")\nbrowser.close()"
      } ]
    }, {
      "name" : "exists",
      "description" : "Returns true when the element is in the page within the wait time, false when it is not. It never raises an error and does not stop the script. Visibility is not checked; use `visible` for that. Waits up to the browser's timeout unless a time is given. Pass at least 1: 0 means \"use the default\".",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum time to wait in milliseconds; 0 or nil uses the browser's timeout",
          "name" : "milliseconds"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nif browser.locator(\"id=cookie-banner\").exists(2000) then\n  browser.locator(\"id=accept\").click()\nend\nbrowser.close()"
      } ]
    }, {
      "name" : "visible",
      "description" : "Returns true when the element is visible, false when it is not or does not exist. It never raises an error and does not stop the script. Waits up to the timeout for visibility on either engine.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum time to wait in milliseconds; 0 or nil uses the browser's timeout",
          "name" : "milliseconds"
        } ]
      } ]
    }, {
      "name" : "enabled",
      "description" : "Returns true when the element is enabled (not disabled), false when it is not or does not exist. It never raises an error and does not stop the script. Waits up to the timeout for the enabled state on either engine.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum time to wait in milliseconds; 0 or nil uses the browser's timeout",
          "name" : "milliseconds"
        } ]
      } ]
    }, {
      "name" : "selected",
      "description" : "Returns true when a checkbox or radio button is ticked, or an option is selected; false when it is not or the element does not exist. It never raises an error and does not stop the script. Waits up to the timeout for the selected state on either engine.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum time to wait in milliseconds; 0 or nil uses the browser's timeout",
          "name" : "milliseconds"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/settings\")\nlocal newsletter = browser.locator(\"id=newsletter\")\nif not newsletter.selected(1000) then\n  newsletter.check()\nend\nbrowser.close()"
      } ]
    }, {
      "name" : "text",
      "description" : "Returns the text the user sees in the element, including its children. Selenium returns an empty string for hidden elements; Playwright returns the element's rendered text. The element is retried until it exists; when it is still missing after the timeout the script stops, or nil is returned after `browser.stopOnFailure(false)`.",
      "returns" : {
        "kind" : "string",
        "description" : "The text, nil after a failure that did not stop the script",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nprint(browser.locator(\"css=h1\").text())\nbrowser.close()"
      } ]
    }, {
      "name" : "attribute",
      "description" : "Returns the value of an HTML attribute as written in the page (for example `href=\"/home\"` gives `/home`, not the full URL), or nil when the element does not have the attribute. To read the live state of the element (the current text of an input, the resolved `href`), use `property`.",
      "returns" : {
        "kind" : "string",
        "description" : "The attribute value, nil when the attribute is absent",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Attribute name, for example `href` or `data-id`",
          "name" : "name"
        } ]
      } ]
    }, {
      "name" : "property",
      "description" : "Returns a property of the live DOM element as a string, for example `value` (what is currently in an input), `checked` (`\"true\"`/`\"false\"`) or `href` (the resolved absolute URL). Returns nil when the property is null or undefined. Use `attribute` for what is written in the HTML.",
      "returns" : {
        "kind" : "string",
        "description" : "The property value as a string, nil when it is null or undefined",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Property name, for example `value` or `checked`",
          "name" : "name"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/login\")\nbrowser.locator(\"id=username\").fill(\"alice\")\nprint(browser.locator(\"id=username\").property(\"value\")) -- alice\nbrowser.close()"
      } ]
    }, {
      "name" : "tag",
      "description" : "Returns the tag name of the element in lower case, for example `input` or `div`.",
      "returns" : {
        "kind" : "string",
        "description" : "The tag name, nil after a failure that did not stop the script",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "execute",
      "description" : "Runs JavaScript with this element as `arguments[0]`, for example `arguments[0].scrollIntoView()`. The return value of the script is discarded; to read a value use `browser.executeJs`, or `property`/`attribute`. The action name for `?Label` selectors in `locators.yaml` is `evaluate`.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JavaScript; the element is available as `arguments[0]`",
          "name" : "script"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com\")\nbrowser.locator(\"id=footer\").execute(\"arguments[0].scrollIntoView()\")\nbrowser.close()"
      } ]
    }, {
      "name" : "enter",
      "description" : "Presses the Enter key on the element, for example to submit a search field. The action name for `?Label` selectors in `locators.yaml` is `buttonEnter`.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "tab",
      "description" : "Presses the Tab key on the element, which moves focus to the next field and fires its blur handlers. The action name for `?Label` selectors in `locators.yaml` is `buttonTab`.",
      "returns" : {
        "kind" : "table",
        "description" : "Result table: [1] is this locator and [2] is true when the action succeeded. A failure stops the script unless `browser.stopOnFailure(false)` was called, in which case [2] is false"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "list",
      "description" : "Returns every element that matches the selector as a table of locators, in document order (the table is empty when nothing matches). Each locator in the list is fixed to its element: it has no selector of its own and cannot be listed again. The page is not waited on for more elements to appear.\n\nOn Selenium, all matches come from the first place that has any: the main page, else the open shadow roots, else the first iframe with a match. Playwright returns all matches, including iframe matches.",
      "returns" : {
        "kind" : "list",
        "ref" : "Locator"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/orders\")\nfor _, row in ipairs(browser.locator(\"css=table.orders tbody tr\").list()) do\n  print(row.text())\nend\nbrowser.close()"
      } ]
    }, {
      "name" : "wait",
      "description" : "Waits until the element is visible and enabled, then returns this locator. Uses action retries, failure screenshots and `stopOnFailure`; `?Label` candidates are resolved on every retry.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Maximum time to wait in milliseconds; 0 or nil uses the browser's timeout",
          "name" : "milliseconds"
        } ]
      } ],
      "examples" : [ {
        "code" : "local browser = Sel.start()\nbrowser.navigate(\"https://example.com/report\")\nbrowser.locator(\"id=download\").wait(30000).click()\nbrowser.close()"
      } ]
    }, {
      "name" : "locator",
      "description" : "Creates a new Locator for another selector. The selector is not relative to this element: it is searched in the whole page, exactly like `browser.locator`. To search inside an element, chain selectors with `>>`.",
      "returns" : {
        "kind" : "object",
        "ref" : "Locator"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Selector, see the Locator description",
          "name" : "selector"
        } ]
      } ]
    } ]
  }, {
    "name" : "SevenZip",
    "kind" : "library",
    "description" : "Extracts `.7z` archives. It can only decompress; there is no function to create a 7z archive (use `Zip` or `Tar` for that).",
    "library" : "SevenZip",
    "category" : "compression",
    "variable" : "SevenZip",
    "functions" : [ {
      "name" : "decompress",
      "description" : "Extracts a `.7z` archive into a directory. Directories are created as needed; empty directories in the archive are not recreated, and existing files are overwritten. Entries are streamed to disk.\n\nLike `Zip.extract` and `Tar.extract`, an entry whose path would end up outside the destination (for example `../x`, or through a symbolic link) aborts the extraction with an error. Throws an error with the reason when the archive is missing or not a valid 7z file; entries extracted before an error stay on disk.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "7z file to extract, relative to the workspace",
          "name" : "in"
        }, {
          "kind" : "string",
          "description" : "Directory to extract into, relative to the workspace",
          "name" : "destination"
        } ]
      } ],
      "examples" : [ {
        "code" : "SevenZip.decompress(\"archive.7z\", \"extracted\")"
      } ]
    } ]
  }, {
    "name" : "Sftp",
    "kind" : "library",
    "description" : "Transfers and manages files on a remote server over SFTP (SSH file transfer). Sftp.newSession returns an SftpSession.\n\nThings to know: the connection is opened lazily by the first operation, and reopened if it was dropped. Host keys: by default any server key is accepted (a warning is logged once per host); pass the server's SHA256 fingerprint as the optional last argument of newSession to pin it, or run the agent with agent.ssh.strictHostKeys=true to require a pinned fingerprint or a known_hosts entry. For key based login call setPrivateKey on the session; the key is a file in the project. Local paths are relative to the project; remote paths are interpreted by the server (relative paths start in the login directory). Failures are raised as errors that contain the remote path and the JSch message. Use secret() for passwords and close sessions with session.close() or Sftp.close().",
    "library" : "Sftp",
    "category" : "files",
    "variable" : "Sftp",
    "functions" : [ {
      "name" : "newSession",
      "description" : "Creates an SFTP session for a server. Nothing is sent yet: connecting and logging in happen at the first operation. For key authentication pass an empty password (\"\") and call setPrivateKey on the session (a key is tried before the password). The optional fingerprint pins the server's SHA256 host key fingerprint (as printed by `ssh-keygen -l`, with or without the \"SHA256:\" prefix); a server with another key is rejected.",
      "returns" : {
        "kind" : "object",
        "ref" : "SftpSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret(), or \"\" when logging in with a key",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address, without sftp://",
          "name" : "host"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret(), or \"\" when logging in with a key",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address, without sftp://",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "SSH port; 22 when left out",
          "name" : "port"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret(), or \"\" when logging in with a key",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address, without sftp://",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "SSH port; 22 when left out",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "SHA256 fingerprint of the server's host key, to pin it",
          "name" : "fingerprint",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local sftp = Sftp.newSession(\"deploy\", secret(\"sftp_password\"), \"files.example.com\")\nsftp.uploadFileTo(\"output/report.csv\", \"/incoming/report.csv\")\nsftp.close()",
        "description" : "Password login and upload"
      }, {
        "code" : "local sftp = Sftp.newSession(\"deploy\", \"\", \"files.example.com\", 2222)\nsftp.setPrivateKey(\"keys/deploy_ed25519\")\nfor _, name in ipairs(sftp.listFiles(\"outgoing\")) do\n    print(name)\nend\nsftp.close()",
        "description" : "Key login on a custom port"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every SFTP session opened with Sftp.newSession in this script.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "SftpSession",
    "kind" : "object",
    "description" : "A session on an SFTP server: upload, download, list, create and delete files and directories. The connection is made by the first operation and re-made when it was dropped or after close(). All operations return the session, so they can be chained.",
    "library" : "Sftp",
    "category" : "files",
    "variable" : "sftp",
    "howToGet" : [ {
      "type" : "Sftp",
      "function" : "newSession",
      "call" : "Sftp.newSession(username, password, host)",
      "returns" : "SftpSession"
    } ],
    "returnedBy" : [ {
      "type" : "Sftp",
      "function" : "newSession"
    } ],
    "functions" : [ {
      "name" : "setPrivateKey",
      "description" : "Adds a private key file for key based login. The path is a file in the project (fetched from the Reflex server when it is not in the workspace). Closes the current connection, so the next operation logs in with the key. Call it before the first operation. Raises \"Failed setting private key: ...\" when the file cannot be read as a key.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Private key file in the project",
          "name" : "privateKey"
        } ]
      } ],
      "examples" : [ {
        "code" : "sftp.setPrivateKey(\"keys/deploy_ed25519\")"
      } ]
    }, {
      "name" : "setPrivateKeyFileWithPassphrase",
      "description" : "Like setPrivateKey, for a key file that is protected by a passphrase. Closes the current connection.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Private key file in the project",
          "name" : "privateKey"
        }, {
          "kind" : "string",
          "description" : "Passphrase of the key; use secret()",
          "name" : "passphrase"
        } ]
      } ],
      "examples" : [ {
        "code" : "sftp.setPrivateKeyFileWithPassphrase(\"keys/deploy_rsa\", secret(\"deploy_key_passphrase\"))"
      } ]
    }, {
      "name" : "downloadFileTo",
      "description" : "Downloads a remote file into the project. Missing local folders are created and an existing local file is overwritten. Raises \"Failed to download: <remote file> Error: ...\" when the remote file does not exist or cannot be read.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file on the server",
          "name" : "remoteFile"
        }, {
          "kind" : "string",
          "description" : "Path in the project to save the file as",
          "name" : "localFile"
        } ]
      } ],
      "examples" : [ {
        "code" : "sftp.downloadFileTo(\"/outgoing/orders.csv\", \"input/orders.csv\")"
      } ]
    }, {
      "name" : "uploadFileTo",
      "description" : "Uploads a project file (fetched from the Reflex server first when it is not in the workspace). The remote file is overwritten when it exists; the remote directory must already exist (use createDirectory). Raises \"Failed to upload <local> to <remote> Error: ...\" on failure.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file in the project",
          "name" : "localFile"
        }, {
          "kind" : "string",
          "description" : "Path on the server to write the file to",
          "name" : "remoteFile"
        } ]
      } ],
      "examples" : [ {
        "code" : "sftp.createDirectory(\"/incoming/2026\")\nsftp.uploadFileTo(\"output/report.csv\", \"/incoming/2026/report.csv\")"
      } ]
    }, {
      "name" : "deleteFile",
      "description" : "Deletes a file on the server. Raises an error when it does not exist or cannot be deleted.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file on the server",
          "name" : "remoteFile"
        } ]
      } ]
    }, {
      "name" : "deleteDirectory",
      "description" : "Deletes a directory on the server. The directory must be empty; delete its files first.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the directory on the server",
          "name" : "remoteDir"
        } ]
      } ]
    }, {
      "name" : "createDirectory",
      "description" : "Creates one directory on the server. It is not recursive: the parent must exist, and an existing directory is an error.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the new directory on the server",
          "name" : "remoteDir"
        } ]
      } ]
    }, {
      "name" : "listFiles",
      "description" : "Lists the names of the entries in a directory, files and subdirectories alike, without the special entries \".\" and \"..\". Only names are returned, no sizes or types. The path is used as given, like in the other functions: a relative path (or \"\" and \".\") starts in the login directory, a path starting with \"/\" in the server root.",
      "returns" : {
        "kind" : "table",
        "description" : "List of entry names"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory on the server; relative to the login directory",
          "name" : "remoteDir"
        } ]
      } ],
      "examples" : [ {
        "code" : "for _, name in ipairs(sftp.listFiles(\"outgoing\")) do\n    if name:match(\"%.csv$\") then\n        sftp.downloadFileTo(\"/outgoing/\" .. name, \"input/\" .. name)\n    end\nend"
      } ]
    }, {
      "name" : "setPermissionsOfTo",
      "description" : "Sets the Unix permissions (chmod) of a remote file or directory. The permissions are an octal string such as \"644\" or \"755\"; anything else is an error.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Path of the file or directory on the server",
          "name" : "remoteFile"
        }, {
          "kind" : "string",
          "description" : "Octal permissions, for example \"644\"",
          "name" : "chmodPermissions"
        } ]
      } ],
      "examples" : [ {
        "code" : "sftp.setPermissionsOfTo(\"/incoming/report.csv\", \"640\")"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes the connection. The next operation connects again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Sharepoint",
    "kind" : "library",
    "description" : "Upload, download, list, move and delete files in SharePoint Online document libraries (Microsoft Graph).\n\n**Setup**: add project secrets for one of these sign-in methods. `Sharepoint.connect(siteUrl)` picks the method from the secrets that exist.\n\n- *Service account*: `sharepoint_username` (e.g. `svc-reflex@contoso.com`) and `sharepoint_password`. The account must be excluded from multi-factor authentication. Optionally `sharepoint_client_id`: an app registration with delegated `Sites.ReadWrite.All` (admin consent) and *Allow public client flows* enabled. Without it, the Microsoft Office client id is used, which some tenants block.\n- *App registration*: `sharepoint_client_id` and `sharepoint_client_secret`, for an app with the application permission `Sites.ReadWrite.All` (or `Sites.Selected` plus a grant on the site) and admin consent.\n\nThe tenant is derived from the site URL; set `sharepoint_tenant_id` only if that fails. Sign-in errors explain what to change (wrong password, MFA required, consent missing, ...).\n\nWhen both kinds of secrets exist, `Sharepoint.connect(siteUrl)` uses the app registration. connect makes network calls (sign-in and resolving the site), so wrong credentials or a wrong URL fail there. Only SharePoint Online (*.sharepoint.com) sites are supported. There is no connection to close. Every request has a 5 minute timeout; errors are raised as \"SharePoint <action> failed: HTTP <status> <code>: <message>\".\n\nRemote paths are relative to the document library (no site URL, no library name). Local paths are relative to the project. Functions that return `this` can be chained: `site.createFolder('A').upload('a.txt', 'A/a.txt')`.",
    "library" : "Sharepoint",
    "category" : "files",
    "variable" : "Sharepoint",
    "functions" : [ {
      "name" : "connect",
      "description" : "Signs in and opens the default document library of a site (usually \"Documents\", shown as \"Shared Documents\" in the URL); switch with useLibrary. With only a site URL, the credentials come from the project secrets (see the setup above): the app registration when sharepoint_client_secret exists, otherwise the service account. With four arguments, signs in as the given app registration (client credentials) and ignores the secrets. Raises an error that explains what to change when the sign-in fails.",
      "returns" : {
        "kind" : "object",
        "ref" : "SharepointSite"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Site URL, e.g. https://contoso.sharepoint.com/sites/Finance",
          "name" : "siteUrl"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Directory (tenant) id or domain, e.g. contoso.onmicrosoft.com",
          "name" : "tenantId"
        }, {
          "kind" : "string",
          "description" : "Application (client) id of the app registration",
          "name" : "clientId"
        }, {
          "kind" : "string",
          "description" : "Client secret of the app registration, e.g. secret('my_secret')",
          "name" : "clientSecret"
        }, {
          "kind" : "string",
          "description" : "Site URL, e.g. https://contoso.sharepoint.com/sites/Finance",
          "name" : "siteUrl"
        } ]
      } ],
      "examples" : [ {
        "code" : "local site = Sharepoint.connect(\"https://contoso.sharepoint.com/sites/Finance\")\nsite.createFolder(\"Reports/2026\")\nsite.upload(\"output/summary.xlsx\", \"Reports/2026/summary.xlsx\")\nfor _, item in ipairs(site.list(\"Inbox\")) do\n    if not item.folder then\n        site.download(item.path, \"input/\" .. item.name)\n        site.move(item.path, \"Processed/\" .. item.name)\n    end\nend"
      } ]
    }, {
      "name" : "connectUser",
      "description" : "Signs in with a service account (user name and password, without multi-factor authentication) and opens the default document library of a site. Uses sharepoint_client_id and sharepoint_tenant_id from the project secrets when present, otherwise the Microsoft Office client id and the tenant derived from the site URL. Use this instead of connect when the credentials are not stored under the standard secret names.",
      "returns" : {
        "kind" : "object",
        "ref" : "SharepointSite"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Username, e.g. svc-reflex@contoso.com",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password, e.g. secret('sharepoint_password')",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Site URL, e.g. https://contoso.sharepoint.com/sites/Finance",
          "name" : "siteUrl"
        } ]
      } ],
      "examples" : [ {
        "code" : "local site = Sharepoint.connectUser(\"svc-reflex@contoso.com\", secret(\"svc_password\"), \"https://contoso.sharepoint.com/sites/Finance\")\nprint(site.exists(\"Reports\"))"
      } ]
    } ]
  }, {
    "name" : "SharepointSite",
    "kind" : "object",
    "description" : "A SharePoint site and document library. Remote paths are relative to the library root, e.g. 'Reports/2026/summary.xlsx'; local paths are relative to the project.",
    "library" : "Sharepoint",
    "category" : "files",
    "variable" : "sharepointSite",
    "howToGet" : [ {
      "type" : "Sharepoint",
      "function" : "connect",
      "call" : "Sharepoint.connect(siteUrl)",
      "returns" : "SharepointSite"
    } ],
    "returnedBy" : [ {
      "type" : "Sharepoint",
      "function" : "connect"
    }, {
      "type" : "Sharepoint",
      "function" : "connectUser"
    } ],
    "functions" : [ {
      "name" : "useLibrary",
      "description" : "Switches to another document library of the site. The name is matched without regard to case against the library's display name and the name in its URL (\"Documents\" or \"Shared Documents\"). Raises an error listing the available libraries when none matches. All later calls on this site use that library.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Library name",
          "name" : "name"
        } ]
      } ],
      "examples" : [ {
        "code" : "site.useLibrary(\"Contracts\")\nprint(#site.list(\"\"))"
      } ]
    }, {
      "name" : "upload",
      "description" : "Uploads a project file to the library, replacing the remote file if it exists. Files are sent in a single request, which SharePoint limits to 250 MB; larger files fail with an HTTP error. The local path must be a file inside the project (otherwise \"Local path must be a file inside the project\"); it is fetched from the Reflex server first when it is not in the workspace. Create missing folders yourself with createFolder.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Local file, relative to the project",
          "name" : "localFile"
        }, {
          "kind" : "string",
          "description" : "Remote file path in the library",
          "name" : "remoteFile"
        } ]
      } ],
      "examples" : [ {
        "code" : "site.createFolder(\"Reports/2026\")\nsite.upload(\"output/summary.xlsx\", \"Reports/2026/summary.xlsx\")"
      } ]
    }, {
      "name" : "download",
      "description" : "Downloads a library file into the project, creating missing local folders and overwriting an existing local file. The local path must be inside the project. Raises an error (\"HTTP 404 itemNotFound\") when the remote file does not exist.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Remote file path in the library",
          "name" : "remoteFile"
        }, {
          "kind" : "string",
          "description" : "Local file, relative to the project",
          "name" : "localFile"
        } ]
      } ],
      "examples" : [ {
        "code" : "site.download(\"Inbox/orders.csv\", \"input/orders.csv\")"
      } ]
    }, {
      "name" : "list",
      "description" : "Lists the files and subfolders directly in a folder (not recursive); \"\" is the library root. Every page of results is read, so large folders are complete. Each item's path can be passed to download, move, exists and delete. Raises an error when the folder does not exist.",
      "returns" : {
        "kind" : "list",
        "ref" : "SharepointItem"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Remote folder path",
          "name" : "folder"
        } ]
      } ],
      "examples" : [ {
        "code" : "for _, item in ipairs(site.list(\"Inbox\")) do\n    if not item.folder and item.name:match(\"%.csv$\") then\n        print(item.path, item.size, item.lastModified)\n    end\nend"
      } ]
    }, {
      "name" : "exists",
      "description" : "Returns true when a file or folder exists at the path, false when SharePoint answers 404. Other errors (no access, sign-in problems) are raised.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Remote path",
          "name" : "path"
        } ]
      } ],
      "examples" : [ {
        "code" : "if not site.exists(\"Reports/2026\") then\n    site.createFolder(\"Reports/2026\")\nend"
      } ]
    }, {
      "name" : "createFolder",
      "description" : "Creates a folder and any missing parent folders. Folders that already exist are left alone, so it is safe to call repeatedly.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Remote folder path",
          "name" : "path"
        } ]
      } ],
      "examples" : [ {
        "code" : "site.createFolder(\"Archive/2026/Q1\")"
      } ]
    }, {
      "name" : "move",
      "description" : "Moves and/or renames a file or folder within the library. The destination path includes the new name, and its parent folder must already exist (createFolder). Raises an error when the destination already exists or the source is missing.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Remote source path",
          "name" : "from"
        }, {
          "kind" : "string",
          "description" : "Remote destination path, including the new name",
          "name" : "to"
        } ]
      } ],
      "examples" : [ {
        "code" : "site.move(\"Inbox/orders.csv\", \"Processed/orders-2026-05.csv\")"
      } ]
    }, {
      "name" : "delete",
      "description" : "Deletes a file or a folder with everything in it; SharePoint moves it to the site recycle bin, so it can be restored there. Refuses to delete the library root (\"\"). Raises an error when the path does not exist.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Remote path",
          "name" : "path"
        } ]
      } ],
      "examples" : [ {
        "code" : "site.delete(\"Inbox/orders.csv\")"
      } ]
    } ]
  }, {
    "name" : "Soap",
    "kind" : "library",
    "description" : "Minimal SOAP 1.1 client: posts an XML envelope to an endpoint and gives back the response body as a string, plus an XPath helper to pick values out of it. There is no WSDL support; build the envelope yourself.\n\nThings to know: only the body text is returned, so the HTTP status is not available, and a SOAP Fault (usually HTTP 500) comes back like any other response, so check the XML for a Fault. Network failures raise \"Api request failed: ...\". There are no timeout, cookie or authentication helpers; add an Authorization header through the headers argument. TLS certificates are always verified; use clientCertificate() for mutual TLS. Credentials belong in secrets: secret(\"name\").",
    "library" : "Soap",
    "category" : "api",
    "variable" : "Soap",
    "functions" : [ {
      "name" : "post",
      "description" : "Posts a SOAP envelope to the endpoint and returns the response body as a string. Sets the headers \"Content-Type: text/xml;charset=UTF-8\" and \"SOAPAction: ACTION\" (the action is sent as given, without added quotes; pass \"\" for none). Entries in the optional headers table replace those defaults, which is how you switch to SOAP 1.2 (\"Content-Type: application/soap+xml\") or add an Authorization header. The request is encoded as UTF-8, the response is read as UTF-8 and carriage returns are removed.\n\nThe last argument is required: with doFormat true the response is pretty-printed, with false it is returned as received. Formatting only changes the indentation; entities such as &amp; keep their meaning. With four arguments the fourth is always doFormat; to pass headers use all five.\n\nThe HTTP status is not returned, and error statuses do not raise: a SOAP Fault is just the response body. Connection problems and timeouts raise \"Api request failed: ...\".",
      "returns" : {
        "kind" : "string",
        "description" : "The response body, formatted when doFormat is true"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL of the SOAP endpoint",
          "name" : "soapUrl"
        }, {
          "kind" : "string",
          "description" : "The complete SOAP envelope as an XML string",
          "name" : "requestMessage"
        }, {
          "kind" : "string",
          "description" : "Value of the SOAPAction header; \"\" for none",
          "name" : "SOAPAction"
        }, {
          "kind" : "boolean",
          "description" : "true to pretty-print the response, false to return it as received",
          "name" : "doFormat"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The URL of the SOAP endpoint",
          "name" : "soapUrl"
        }, {
          "kind" : "string",
          "description" : "The complete SOAP envelope as an XML string",
          "name" : "requestMessage"
        }, {
          "kind" : "string",
          "description" : "Value of the SOAPAction header; \"\" for none",
          "name" : "SOAPAction"
        }, {
          "kind" : "table",
          "description" : "Extra request headers. A map ({[\"Content-Type\"] = \"application/soap+xml\"}) replaces the default header of that name; a list of \"Name:value\" strings adds headers",
          "name" : "headers"
        }, {
          "kind" : "boolean",
          "description" : "true to pretty-print the response, false to return it as received",
          "name" : "doFormat"
        } ]
      } ],
      "examples" : [ {
        "code" : "local envelope = [[\n<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ord=\"urn:orders\">\n  <soapenv:Body>\n    <ord:GetOrder><ord:OrderId>1001</ord:OrderId></ord:GetOrder>\n  </soapenv:Body>\n</soapenv:Envelope>]]\n\nlocal response = Soap.post(\n    \"https://erp.example.com/services/orders\",\n    envelope,\n    \"urn:orders/GetOrder\",\n    { Authorization = \"Bearer \" .. secret(\"erp_token\") },\n    false)\nAssert.equals(\"\", Soap.xpath(response, \"//*[local-name()='Fault']\"))\nprint(Soap.xpath(response, \"//*[local-name()='Status']\"))",
        "description" : "Call a service with an Authorization header and check the result"
      } ]
    }, {
      "name" : "clientCertificate",
      "description" : "Registers a PKCS#12 (.p12/.pfx) client certificate for mutual TLS. It is presented only on requests to the given host and port (a URL without a port means 443 for https and 80 for http); calling it again for the same host and port replaces the certificate. The file is read from the project (fetched from the Reflex server when it is not in the workspace). A copy fetched from the server is removed from the workspace after loading; a file that was already in the workspace is left alone. Raises \"Failed to configure client certificate: ...\" when the port is outside 1-65535, the file is missing, or the passphrase is wrong. Use secret() for the passphrase.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Host name of the HTTPS endpoint, without scheme or port",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "Port of the endpoint, 1-65535",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "PKCS#12 file in the project",
          "name" : "file"
        }, {
          "kind" : "string",
          "description" : "Passphrase of the file; nil is treated as an empty one",
          "name" : "passphrase"
        } ]
      } ],
      "examples" : [ {
        "code" : "Soap.clientCertificate(\"soap.partner.example\", 443, \"certs/client.p12\", secret(\"client_cert_passphrase\"))\nlocal response = Soap.post(\"https://soap.partner.example/service\", \"<Envelope/>\", \"\", false)"
      } ]
    }, {
      "name" : "cert",
      "description" : "Alias for clientCertificate(host, port, file, passphrase).",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Host name of the HTTPS endpoint, without scheme or port",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "Port of the endpoint, 1-65535",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "PKCS#12 file in the project",
          "name" : "file"
        }, {
          "kind" : "string",
          "description" : "Passphrase of the file; nil is treated as an empty one",
          "name" : "passphrase"
        } ]
      } ]
    }, {
      "name" : "xpath",
      "description" : "Evaluates an XPath 1.0 expression on an XML string and returns the result as a string: the text of the first matching node, or the string form of a number, boolean or string expression (count(//Item) gives \"3\"). Returns \"\" when nothing matches. Raises an error when the XML cannot be parsed or the expression is invalid.\n\nThe XML is parsed with namespace support. Prefixes declared in the document can be used in the expression (//ord:OrderId), and a third argument maps extra prefixes to namespace URIs, which is the way to address elements in a default namespace (xmlns=\"urn:orders\") because XPath 1.0 has no default namespace. Matching on the local name keeps working: //*[local-name()='OrderId']. A name without a prefix only matches elements that are in no namespace. Entities are decoded, so an ampersand comes back as \"&\".",
      "returns" : {
        "kind" : "string",
        "description" : "The result of the XPath expression, or \"\" when nothing matches"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The XML document as a string",
          "name" : "in"
        }, {
          "kind" : "string",
          "description" : "The XPath 1.0 expression",
          "name" : "xpath"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The XML document as a string",
          "name" : "in"
        }, {
          "kind" : "string",
          "description" : "The XPath 1.0 expression",
          "name" : "xpath"
        }, {
          "kind" : "table",
          "description" : "Extra namespace prefixes as a table `{ prefix = \"uri\" }`, added to the prefixes declared in the document",
          "name" : "namespaces",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local xml = '<Envelope><Body><GetOrderResponse><OrderId>42</OrderId><Status>SHIPPED</Status></GetOrderResponse></Body></Envelope>'\nprint(Soap.xpath(xml, \"//OrderId\"))\nprint(Soap.xpath(xml, \"count(//*)\"))\nlocal ns = '<o:Order xmlns:o=\"urn:orders\"><o:Id>7</o:Id></o:Order>'\nprint(Soap.xpath(ns, \"//o:Id\"))\nprint(Soap.xpath('<Order xmlns=\"urn:orders\"><Id>8</Id></Order>', \"//x:Id\", { x = \"urn:orders\" }))"
      } ]
    } ]
  }, {
    "name" : "Sql",
    "kind" : "library",
    "description" : "Connects to relational databases over JDBC and runs SQL on them. Sql.connect returns a SqlConnection with query, execute and transaction functions.\n\nThings to know: the agent does not bundle JDBC drivers. Add the driver JAR of your database (PostgreSQL, MySQL, SQL Server, Oracle, ...) to the `plugins/` folder next to agent.jar, or put it on the agent's classpath, and restart the agent; otherwise connect fails with \"Failed to open SQL connection: No suitable driver found for ...\". Always pass values as ? parameters instead of building SQL strings. Errors from the driver are raised as Lua errors with the driver's message. Connections are not closed automatically: call close() on the connection, or Sql.close() to close every connection opened in this script. Use secret() for passwords.",
    "library" : "Sql",
    "category" : "databases",
    "variable" : "Sql",
    "functions" : [ {
      "name" : "connect",
      "description" : "Opens a JDBC connection and returns it. The driver for the URL must be available: JAR files in the `plugins/` folder (or on the classpath) are picked up, none is bundled. Raises an error when the URL is empty, the driver class is not found, no driver accepts the URL (\"No suitable driver\") or the database refuses the connection (\"Failed to open SQL connection: ...\").\n\nCredentials can be passed as separate user and password arguments, or inside the options table. The options table understands: user, password, driver (a driver class name to load first; modern drivers register themselves, so it is rarely needed), autoCommit (boolean; the driver default, normally true, when left out), properties (a table of extra JDBC properties) and every other string key, which is passed to the driver as a JDBC property (for example ssl = true or ApplicationName = \"reflex\"). Property values are converted to strings.",
      "returns" : {
        "kind" : "object",
        "ref" : "SqlConnection"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "JDBC URL, for example jdbc:postgresql://host:5432/db",
          "name" : "url"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "JDBC URL, for example jdbc:postgresql://host:5432/db",
          "name" : "url"
        }, {
          "kind" : "table",
          "description" : "Connection options: user, password, driver, autoCommit, properties; any other string key is passed to the driver as a JDBC property",
          "name" : "options"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "JDBC URL, for example jdbc:postgresql://host:5432/db",
          "name" : "url"
        }, {
          "kind" : "string",
          "description" : "Database user name",
          "name" : "user"
        }, {
          "kind" : "string",
          "description" : "Database password; use secret()",
          "name" : "password"
        } ]
      } ],
      "examples" : [ {
        "code" : "local db = Sql.connect(\"jdbc:postgresql://db.example.com:5432/shop\", \"reporter\", secret(\"db_password\"))\nlocal rows = db.query(\"select id, name from customers where country = ?\", { \"NL\" })\nfor _, row in ipairs(rows) do\n    print(row.id, row.name)\nend\ndb.close()",
        "description" : "Connect with user and password, run a parameterised query"
      }, {
        "code" : "local db = Sql.connect(\"jdbc:sqlserver://sql.example.com:1433;databaseName=shop\", {\n    user = \"reporter\",\n    password = secret(\"db_password\"),\n    encrypt = true,\n    autoCommit = false,\n})\ndb.execute(\"update stock set quantity = quantity - ? where sku = ?\", { 1, \"A-100\" })\ndb.commit()\ndb.close()",
        "description" : "Connect with an options table and manage the transaction yourself"
      } ]
    }, {
      "name" : "nullValue",
      "description" : "Returns a marker that stands for SQL NULL. Pass it in the parameter list of query or execute to bind NULL: a Lua nil cannot be used for this, because a nil in the middle of a list leaves a gap and the list is rejected (\"must contain contiguous indexes from 1\"). With a type name the NULL is bound with that SQL type, which some drivers need (VARCHAR, INTEGER, BIGINT, TIMESTAMP and the other java.sql.Types names, case-insensitive); an unknown name raises an error.\n\nQuery results use the same marker for NULL column values, as a table {__sqlNull = true}. A NULL column is therefore not nil: test it with type(value) == \"table\" and value.__sqlNull.",
      "returns" : {
        "kind" : "table",
        "description" : "A marker table ({__sqlNull = true}), with sqlType set when a type was given"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "SQL type name such as VARCHAR, INTEGER or TIMESTAMP",
          "name" : "sqlType"
        } ]
      } ],
      "examples" : [ {
        "code" : "local db = Sql.connect(\"jdbc:postgresql://db.example.com/shop\", \"app\", secret(\"db_password\"))\ndb.execute(\"update customers set phone = ? where id = ?\", { Sql.nullValue(\"VARCHAR\"), 42 })\ndb.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every connection opened with Sql.connect in this script that is still open. Errors while closing are ignored. Changes that were not committed are not committed by this call.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "SqlConnection",
    "kind" : "object",
    "description" : "An open JDBC connection, returned by Sql.connect. Run SELECTs with query and everything else with execute; both take ? placeholders and a list of values. A new connection is in auto-commit mode (every statement is committed on its own) unless connect was given autoCommit = false; use begin, commit and rollback to group statements. Close the connection when you are done.",
    "library" : "Sql",
    "category" : "databases",
    "variable" : "db",
    "howToGet" : [ {
      "type" : "Sql",
      "function" : "connect",
      "call" : "Sql.connect(url)",
      "returns" : "SqlConnection"
    } ],
    "returnedBy" : [ {
      "type" : "Sql",
      "function" : "connect"
    } ],
    "functions" : [ {
      "name" : "query",
      "description" : "Runs a statement that returns rows (SELECT) and returns all rows at once. The result is a list: ipairs(rows) walks the rows, rows.count (also #rows) is the number of rows, and rows.columns lists the columns as {index, label, name, type} tables. A row can be read by column label (row.name, where the label is the alias when the query has one) or by position (row[1]); when two columns share a label, the last one wins by label.\n\nValues are converted as follows: integers and floating point numbers become numbers; DECIMAL becomes a number when it is a whole number that fits a long, or has decimals and at most 15 digits, otherwise a string with the exact value (so no precision is lost); dates and times become ISO 8601 strings, and timestamps become UTC instants like 2025-03-01T08:30:00Z (a timestamp without time zone is first read in the agent's time zone); binary data becomes a Base64 string; booleans stay booleans; anything else is converted with its text form. SQL NULL becomes the marker table {__sqlNull = true} (see Sql.nullValue), not nil.\n\nParameters replace the ? placeholders, in order, and must be a list without gaps: strings, numbers and booleans bind as such, Sql.nullValue() binds NULL, and a table is encoded with Json.encode and bound as a string (useful for JSON columns). Named parameters are not supported. The whole result is held in memory, so add a LIMIT for large tables. Raises \"SQL query failed: <driver message>\" when the statement fails or returns no result set.",
      "returns" : {
        "kind" : "table",
        "description" : "List of rows, each a table of column label and position to value"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "SQL statement that returns rows, with ? placeholders",
          "name" : "sql"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "SQL statement that returns rows, with ? placeholders",
          "name" : "sql"
        }, {
          "kind" : "table",
          "description" : "Values for the ? placeholders, as a list without gaps; use Sql.nullValue() for NULL",
          "name" : "params"
        } ]
      } ],
      "examples" : [ {
        "code" : "local rows = db.query(\"select o.id, o.total, c.name as customer from orders o join customers c on c.id = o.customer_id where o.status = ? and o.total > ?\", { \"open\", 100 })\nprint(\"open orders: \" .. rows.count)\nfor _, row in ipairs(rows) do\n    print(row.id, row.customer, row.total)\nend"
      } ]
    }, {
      "name" : "execute",
      "description" : "Runs a statement that does not return rows: INSERT, UPDATE, DELETE or DDL such as CREATE TABLE. Returns the number of affected rows (0 for DDL). Parameters work as in query. In auto-commit mode the change is committed immediately, otherwise it is part of the open transaction until commit() or rollback(). Raises \"SQL execute failed: <driver message>\" when the statement fails.",
      "returns" : {
        "kind" : "number",
        "description" : "The number of affected rows"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "SQL statement that returns no rows, with ? placeholders",
          "name" : "sql"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "SQL statement that returns no rows, with ? placeholders",
          "name" : "sql"
        }, {
          "kind" : "table",
          "description" : "Values for the ? placeholders, as a list without gaps; use Sql.nullValue() for NULL",
          "name" : "params"
        } ]
      } ],
      "examples" : [ {
        "code" : "local changed = db.execute(\"update orders set status = ? where status = ? and created < ?\", { \"expired\", \"open\", \"2025-01-01\" })\nAssert.equals(3, changed)"
      } ]
    }, {
      "name" : "begin",
      "description" : "Starts a transaction by switching auto-commit off. Nothing is committed until commit(); rollback() discards the changes. Auto-commit stays off after commit and rollback, so the next statements start a new transaction; switch back with autoCommit(true).",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "db.begin()\ndb.execute(\"insert into audit(event) values (?)\", { \"start import\" })\ndb.execute(\"insert into items(sku) values (?)\", { \"A-100\" })\ndb.commit()"
      } ]
    }, {
      "name" : "commit",
      "description" : "Commits the changes made since the last commit or rollback. Raises an error when the connection is closed or in auto-commit mode (JDBC drivers reject a commit there), so call begin() or autoCommit(false) first.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "rollback",
      "description" : "Discards the changes made since the last commit or rollback. Raises an error when the connection is closed or in auto-commit mode (JDBC drivers reject a rollback there), so call begin() or autoCommit(false) first.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "db.begin()\nlocal ok, err = pcall(function()\n    db.execute(\"delete from items where batch = ?\", { 7 })\n    db.execute(\"insert into items(sku, batch) values (?, ?)\", { \"A-100\", 7 })\nend)\nif ok then db.commit() else db.rollback() end"
      } ]
    }, {
      "name" : "autoCommit",
      "description" : "Switches auto-commit on or off. With auto-commit on every statement is committed on its own; with it off statements are collected in a transaction (same as begin()). As JDBC specifies, switching it on while a transaction is open commits that transaction.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "boolean",
          "description" : "true for auto-commit, false for manual transactions",
          "name" : "enabled"
        } ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes the connection and releases it. Pending changes are not committed by Reflex (what the driver does with them is up to the driver), so commit() first when you need them. Closing twice is harmless, errors while closing are ignored, and any other call on a closed connection raises \"SQL connection is already closed\".",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local db = Sql.connect(\"jdbc:postgresql://db.example.com/shop\", \"reporter\", secret(\"db_password\"))\nlocal ok, err = pcall(function()\n    Assert.equals(1, db.query(\"select count(*) as n from customers where id = ?\", { 42 })[1].n)\nend)\ndb.close()\nif not ok then error(err) end"
      } ]
    } ]
  }, {
    "name" : "Ssh",
    "kind" : "library",
    "description" : "Runs commands on remote servers over SSH. Ssh.newSession (password) and Ssh.newSessionWithKey (private key) return an SshSession.\n\nThings to know: the connection is opened lazily by the first command, so wrong credentials or an unreachable host show up there, not in newSession. Host keys: by default any server key is accepted (a warning is logged once per host); pass the server's SHA256 fingerprint as the optional last argument of newSession to pin it, or run the agent with agent.ssh.strictHostKeys=true to require a pinned fingerprint or a known_hosts entry. A command is stopped after 300 seconds unless you pass another time limit, and session.run also returns the exit code. Keep passwords and private keys in secrets: secret(\"name\"). Close sessions with session.close() or Ssh.close().",
    "library" : "Ssh",
    "category" : "files",
    "variable" : "Ssh",
    "functions" : [ {
      "name" : "newSession",
      "description" : "Creates an SSH session that logs in with a password. Nothing is sent over the network yet: the connection and login happen when the first command is executed. The optional fingerprint is the             server's SHA256 host key fingerprint (as printed by `ssh-keygen -l`, with or without the \"SHA256:\"             prefix); when given, a server with another key is rejected.",
      "returns" : {
        "kind" : "object",
        "ref" : "SshSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret()",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address of the server",
          "name" : "host"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret()",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address of the server",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "SSH port; 22 when left out",
          "name" : "port"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Password; use secret()",
          "name" : "password"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address of the server",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "SSH port; 22 when left out",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "SHA256 fingerprint of the server's host key, to pin it",
          "name" : "fingerprint",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local session = Ssh.newSession(\"deploy\", secret(\"deploy_password\"), \"app01.example.com\")\nprint(session.execTrimmed(\"uname -a\"))\nsession.close()"
      } ]
    }, {
      "name" : "newSessionWithKey",
      "description" : "Creates an SSH session that logs in with a private key. The key is the text of the key itself (the \"-----BEGIN ... PRIVATE KEY-----\" block), not a file path, so store it in a secret. Keys protected by a passphrase are not supported. Connection and login happen when the first command is executed. The optional fingerprint pins the server's SHA256 host key fingerprint, as in newSession.",
      "returns" : {
        "kind" : "object",
        "ref" : "SshSession"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Private key text (PEM/OpenSSH), from a secret",
          "name" : "sshKey"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address of the server",
          "name" : "host"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Private key text (PEM/OpenSSH), from a secret",
          "name" : "sshKey"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address of the server",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "SSH port; 22 when left out",
          "name" : "port"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "User name on the server",
          "name" : "username"
        }, {
          "kind" : "string",
          "description" : "Private key text (PEM/OpenSSH), from a secret",
          "name" : "sshKey"
        }, {
          "kind" : "string",
          "description" : "Host name or IP address of the server",
          "name" : "host"
        }, {
          "kind" : "number",
          "description" : "SSH port; 22 when left out",
          "name" : "port"
        }, {
          "kind" : "string",
          "description" : "SHA256 fingerprint of the server's host key, to pin it",
          "name" : "fingerprint",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local session = Ssh.newSessionWithKey(\"deploy\", secret(\"deploy_private_key\"), \"app01.example.com\", 2222)\nlocal status = session.execTrimmed(\"systemctl is-active myapp\")\nAssert.equals(\"active\", status)\nsession.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every SSH session opened with Ssh.newSession or Ssh.newSessionWithKey in this script.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "SshSession",
    "kind" : "object",
    "description" : "An SSH connection to one server, used to run commands. Each command runs in its own channel, so state such as the working directory or exported variables does not carry over to the next command; chain commands in one call (\"cd /opt/app && ls\"). If the connection was closed or dropped, the next command reconnects.",
    "library" : "Ssh",
    "category" : "files",
    "variable" : "session",
    "howToGet" : [ {
      "type" : "Ssh",
      "function" : "newSession",
      "call" : "Ssh.newSession(username, password, host)",
      "returns" : "SshSession"
    } ],
    "returnedBy" : [ {
      "type" : "Ssh",
      "function" : "newSession"
    }, {
      "type" : "Ssh",
      "function" : "newSessionWithKey"
    } ],
    "functions" : [ {
      "name" : "exec",
      "description" : "Runs a command on the server and returns its output as a string. The call waits until the command has finished, for at most the timeout (300 seconds unless you pass another number of seconds as third argument); a command that runs longer is stopped and raises \"Command timed out\". The output is the standard output; when that is empty, the standard error output is returned instead. The exit code is not returned here: use `run` when you need it. Raises \"Failed to execute command: ... Error: ...\" when connecting, logging in or running fails.",
      "returns" : {
        "kind" : "string",
        "description" : "Standard output of the command, or standard error when stdout is empty"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        }, {
          "kind" : "boolean",
          "description" : "true to remove leading and trailing whitespace from the output; default false",
          "name" : "trimOutput"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        }, {
          "kind" : "boolean",
          "description" : "true to remove leading and trailing whitespace from the output; default false",
          "name" : "trimOutput"
        }, {
          "kind" : "number",
          "description" : "Time limit in seconds; default 300",
          "name" : "timeout"
        } ]
      } ],
      "examples" : [ {
        "code" : "local listing = session.exec(\"ls -1 /var/backups\")\nlocal host = session.exec(\"hostname\", true)\nlocal report = session.exec(\"./long-report.sh\", true, 900)"
      } ]
    }, {
      "name" : "execTrimmed",
      "description" : "Same as exec(command, true): runs the command and returns the output without leading and trailing whitespace (such as the final newline), which is what you want for single values. The optional second argument is the time limit in seconds (default 300).",
      "returns" : {
        "kind" : "string",
        "description" : "The command output without leading and trailing whitespace"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        }, {
          "kind" : "number",
          "description" : "Time limit in seconds; default 300",
          "name" : "timeout"
        } ]
      } ],
      "examples" : [ {
        "code" : "local version = session.execTrimmed(\"cat /opt/app/VERSION\")"
      } ]
    }, {
      "name" : "run",
      "description" : "Runs a command and returns a table with its exit code and both output streams, for scripts that need to know whether the command succeeded. The exit code is -1 when the server did not report one. The time limit is 300 seconds unless you pass another number of seconds as second argument; a command that runs longer is stopped and raises \"Command timed out\". Raises \"Failed to execute command: ...\" when connecting, logging in or running fails; a non-zero exit code is not an error.",
      "returns" : {
        "kind" : "shape",
        "ref" : "SshResult"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Command line, run by the user's shell on the server",
          "name" : "command"
        }, {
          "kind" : "number",
          "description" : "Time limit in seconds; default 300",
          "name" : "timeout"
        } ]
      } ],
      "examples" : [ {
        "code" : "local result = session.run(\"systemctl is-active myapp\")\nAssert.equals(0, result.exitCode, result.stderr)\nprint(result.stdout)"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes the connection to the server. Errors while closing are ignored. Running another command afterwards connects again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "String",
    "kind" : "library",
    "description" : "Regular expressions, text replacement, Base64 and XML formatting for strings.\n\nNot to be confused with Lua's built-in lowercase `string` library. Patterns here are Java regular expressions, not Lua patterns, and a backslash must be doubled in Lua source: write `\"\\\\d+\"` for `\\d+`.",
    "library" : "String",
    "category" : "strings",
    "variable" : "String",
    "functions" : [ {
      "name" : "regex",
      "description" : "Returns the first match of a regular expression, or the text of one of its capture groups. When there is no match, or the requested group did not take part in the match, the result is the text `Not found`, not `nil`; compare against that text or use `String.regexMatch`.\n\nGroup 0 is the whole match, group 1 the first set of parentheses. A group number beyond the number of groups throws `No group n`, and an invalid expression throws the pattern error.",
      "returns" : {
        "kind" : "string",
        "description" : "The first match or group text, or `Not found`"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to search",
          "name" : "lineLua"
        }, {
          "kind" : "string",
          "description" : "Java regular expression",
          "name" : "patternLua"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to search",
          "name" : "lineLua"
        }, {
          "kind" : "string",
          "description" : "Java regular expression",
          "name" : "patternLua"
        }, {
          "kind" : "number",
          "description" : "Capture group to return: 0 is the whole match, 1 the first group",
          "name" : "groupLua"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(String.regex(\"order-1234\", \"\\\\d+\"))\nprint(String.regexMatch(\"abc\", \"a.c\"))\nprint(String.replace(\"a-b-c\", \"-\", \"+\"))\nprint(String.decode(String.encode(\"hello\")))",
        "runnable" : true
      } ]
    }, {
      "name" : "regexArray",
      "description" : "Returns the first match of a regular expression together with its capture groups, as a list: item 1 is the whole match, items 2 and up are the groups in order. Only the first match in the text is used, not every match. The list is empty when nothing matches, and a group that did not take part in the match leaves a hole (`nil`) in the list.",
      "returns" : {
        "kind" : "table",
        "description" : "List: whole match, then the capture groups; empty when there is no match"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to search",
          "name" : "lineLua"
        }, {
          "kind" : "string",
          "description" : "Java regular expression",
          "name" : "patternLua"
        } ]
      } ],
      "examples" : [ {
        "code" : "local parts = String.regexArray(\"john@example.nl\", \"(\\\\w+)@(\\\\w+)\\\\.(\\\\w+)\")\nprint(parts[1], parts[2], parts[3], parts[4])",
        "runnable" : true
      } ]
    }, {
      "name" : "regexMatch",
      "description" : "Tells whether the whole text matches the regular expression. It is not enough that a part matches: `abcd` does not match `a.c`, use `.*c.*` for that.",
      "returns" : {
        "kind" : "boolean"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to test",
          "name" : "lineLua"
        }, {
          "kind" : "string",
          "description" : "Java regular expression the whole text must match",
          "name" : "patternLua"
        } ]
      } ]
    }, {
      "name" : "replace",
      "description" : "Replaces every occurrence of a piece of text. The search text is literal, not a pattern, so `.` only matches a dot. All three arguments are required: a missing replacement is inserted as the text `nil`.",
      "returns" : {
        "kind" : "string",
        "description" : "The text with all occurrences replaced"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to change",
          "name" : "string"
        }, {
          "kind" : "string",
          "description" : "Text to look for (literal)",
          "name" : "replace"
        }, {
          "kind" : "string",
          "description" : "Text to put in its place, may be empty",
          "name" : "with"
        } ]
      } ]
    }, {
      "name" : "encode",
      "description" : "Encodes text as standard Base64 with padding. The text is converted to bytes as UTF-8 first.",
      "returns" : {
        "kind" : "string",
        "description" : "The Base64 text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to encode",
          "name" : "value"
        } ]
      } ]
    }, {
      "name" : "decode",
      "description" : "Decodes standard Base64 (with or without padding) into text, read in the JVM default charset (UTF-8 on current Java versions). Throws on characters that are not Base64, such as spaces or line breaks.",
      "returns" : {
        "kind" : "string",
        "description" : "The decoded text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Base64 text to decode",
          "name" : "value"
        } ]
      } ]
    }, {
      "name" : "formatXml",
      "description" : "Re-indents XML text. Without a second argument it indents with two spaces. Throws a parser error when the text is not well-formed XML. To work with the content of XML, use `Xml`.",
      "returns" : {
        "kind" : "string",
        "description" : "The formatted XML text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Well-formed XML text",
          "name" : "xml"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Well-formed XML text",
          "name" : "xml"
        }, {
          "kind" : "number",
          "description" : "Indentation: 1 tabs, 2 two spaces, 3 three spaces, 4 four spaces, any other number gives compact output on one line",
          "name" : "indent"
        } ]
      } ],
      "examples" : [ {
        "code" : "print(String.formatXml(\"<a><b>1</b></a>\"))\nprint(String.formatXml(\"<a><b>1</b></a>\", 0))",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Tar",
    "kind" : "library",
    "description" : "Creates and extracts `.tar` archives, optionally gzip-compressed, in the project workspace.\n\nThe archive is compressed with gzip when its name ends with `.tgz`, `.tar.gz` or `.gz` (checked ignoring case), for both creating and extracting; any other name gives a plain tar. Archiving a directory stores it under its own name, and extracting never writes outside the destination directory. Symbolic and hard links are kept as links; other special entries (devices, FIFOs) are skipped.",
    "library" : "Tar",
    "category" : "compression",
    "variable" : "Tar",
    "functions" : [ {
      "name" : "create",
      "description" : "Archives a file or a whole directory into a tar file. A file is stored under its file name; a directory is stored with its own name as the top folder. The archive is replaced if it exists, its missing parent directories are created, and the archive itself is left out when it lies inside the source directory. Use a name ending in `.tar.gz` or `.tgz` for a compressed archive.\n\nThrows `Failed to create tar archive` when the source does not exist or the archive cannot be written.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory to archive, relative to the workspace",
          "name" : "source"
        }, {
          "kind" : "string",
          "description" : "Archive to create, relative to the workspace; `.tar.gz` or `.tgz` compresses",
          "name" : "destination"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"hello.txt\", \"hello\")\nTar.create(\"hello.txt\", \"hello.tar\")\nTar.extract(\"hello.tar\", \"untarred\")\nprint(Fl.exists(\"untarred/hello.txt\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "extract",
      "description" : "Extracts a tar archive (gzip-compressed when the name ends in `.tgz`, `.tar.gz` or `.gz`) into a directory, creating the directory when needed. Existing files with the same name are overwritten.\n\nAn entry whose path would end up outside the destination, or a link pointing outside it, aborts the extraction. Throws `Failed to extract tar archive` when the archive is missing or damaged. A file that is not a tar archive but has a name that does not end in `.gz` may be accepted and extract nothing.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Archive to extract, relative to the workspace",
          "name" : "archive"
        }, {
          "kind" : "string",
          "description" : "Directory to extract into, relative to the workspace",
          "name" : "destination"
        } ]
      } ]
    } ]
  }, {
    "name" : "Telegram",
    "kind" : "library",
    "description" : "Sends messages to a Telegram chat through a bot and waits for the answer, for notifications and simple human-in-the-loop approvals. Telegram.bot returns a TelegramBot tied to one chat.\n\nSetup: create a bot with @BotFather to get a token, send the bot a message from the chat that should receive the notifications, and find that chat's numeric id (bot.poll() shows chat_id of the incoming message). Keep the token in a secret: secret(\"name\").\n\nThings to know: answers are read with long polling (getUpdates). A bot that has a webhook set, or that is read by another running script, does not receive updates here. Updates are consumed once they have been read. All calls go to api.telegram.org and raise \"Telegram API error: ...\" on failure.",
    "library" : "Telegram",
    "category" : "messaging",
    "variable" : "Telegram",
    "functions" : [ {
      "name" : "bot",
      "description" : "Creates a bot for one chat and checks the token with the Telegram getMe call, so an invalid token raises \"Telegram API error: Unauthorized\" here. The chat id must be a number or numeric text; negative ids (groups) work, and a non-numeric value such as \"@mychannel\" raises an error. Every message the bot sends goes to this chat, and only replies from this chat are read.",
      "returns" : {
        "kind" : "object",
        "ref" : "TelegramBot"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Bot token from @BotFather; use secret()",
          "name" : "token"
        }, {
          "kind" : "string",
          "description" : "Numeric id of the chat to talk to (negative for groups)",
          "name" : "chatId"
        } ]
      } ],
      "examples" : [ {
        "code" : "local bot = Telegram.bot(secret(\"telegram_token\"), secret(\"telegram_chat_id\"))\nbot.send(\"Nightly run finished: 42 checks passed\")\nbot.close()"
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every bot created with Telegram.bot in this script. A closed bot cannot be used again.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "TelegramBot",
    "kind" : "object",
    "description" : "A Telegram bot bound to one chat. Send text with send, ask a question with buttons and wait for the answer with ask + waitCallback, or wait for a typed answer with waitReply. Messages are represented as tables with message_id, chat_id, text and date; keep the table returned by send/ask to edit the message or wait for its buttons.",
    "library" : "Telegram",
    "category" : "messaging",
    "variable" : "bot",
    "howToGet" : [ {
      "type" : "Telegram",
      "function" : "bot",
      "call" : "Telegram.bot(token, chatId)",
      "returns" : "TelegramBot"
    } ],
    "returnedBy" : [ {
      "type" : "Telegram",
      "function" : "bot"
    } ],
    "functions" : [ {
      "name" : "me",
      "description" : "Asks Telegram who the bot is (getMe) and returns its id, username and first name. Raises an error when the token is no longer valid.",
      "returns" : {
        "kind" : "shape",
        "ref" : "TelegramUser"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "print(bot.me().username)"
      } ]
    }, {
      "name" : "send",
      "description" : "Sends a text message to the chat and returns the sent message. The text is limited by Telegram to 4096 characters. Without a parse mode the text is plain; with \"HTML\" or \"Markdown\" (or \"MarkdownV2\") Telegram formats it, and text that is not valid in that mode is rejected with an error, so escape special characters.",
      "returns" : {
        "kind" : "shape",
        "ref" : "TelegramMessage"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Message text, up to 4096 characters",
          "name" : "text"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Message text, up to 4096 characters",
          "name" : "text"
        }, {
          "kind" : "string",
          "description" : "\"HTML\", \"Markdown\" or \"MarkdownV2\"; plain text when left out",
          "name" : "parseMode"
        } ]
      } ],
      "examples" : [ {
        "code" : "bot.send(\"<b>Nightly run</b> finished: 42 passed, 0 failed\", \"HTML\")"
      } ]
    }, {
      "name" : "ask",
      "description" : "Sends a message with buttons under it and returns the sent message; pass that table to waitCallback to get the pressed button. Buttons are a list of {label, callback_data} pairs. All buttons are in one row, unless you put the string \"---\" in the list to start a new row. callback_data is what waitCallback returns (Telegram allows 1 to 64 bytes). Anyone in the chat can press the buttons.",
      "returns" : {
        "kind" : "shape",
        "ref" : "TelegramMessage"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Message text",
          "name" : "text"
        }, {
          "kind" : "table",
          "description" : "Buttons: {{\"Label\", \"callback_data\"}, ...}; \"---\" starts a new row",
          "name" : "buttons"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Message text",
          "name" : "text"
        }, {
          "kind" : "table",
          "description" : "Buttons: {{\"Label\", \"callback_data\"}, ...}; \"---\" starts a new row",
          "name" : "buttons"
        }, {
          "kind" : "string",
          "description" : "\"HTML\", \"Markdown\" or \"MarkdownV2\"; plain text when left out",
          "name" : "parseMode"
        } ]
      } ],
      "examples" : [ {
        "code" : "local question = bot.ask(\"Deploy build 1.4.2 to production?\", {\n    { \"Deploy\", \"yes\" }, { \"Cancel\", \"no\" },\n    \"---\",\n    { \"Ask me later\", \"later\" },\n})\nlocal answer = bot.waitCallback(question, 600)\nif answer == \"yes\" then\n    bot.edit(question, \"Deploying build 1.4.2\")\nelse\n    bot.edit(question, \"Not deployed (\" .. tostring(answer) .. \")\")\nend"
      } ]
    }, {
      "name" : "edit",
      "description" : "Changes the text of a message the bot sent in this chat (only its message_id is used) and returns the updated message. The buttons are removed unless you pass new ones as the fourth argument. Telegram rejects an edit that leaves the text unchanged (\"message is not modified\").",
      "returns" : {
        "kind" : "shape",
        "ref" : "TelegramMessage"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "TelegramMessage",
          "description" : "The message table returned by send or ask",
          "name" : "message"
        }, {
          "kind" : "string",
          "description" : "New text",
          "name" : "text"
        } ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "TelegramMessage",
          "description" : "The message table returned by send or ask",
          "name" : "message"
        }, {
          "kind" : "string",
          "description" : "New text",
          "name" : "text"
        }, {
          "kind" : "string",
          "description" : "\"HTML\", \"Markdown\" or \"MarkdownV2\"; plain text when left out",
          "name" : "parseMode"
        } ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "TelegramMessage",
          "description" : "The message table returned by send or ask",
          "name" : "message"
        }, {
          "kind" : "string",
          "description" : "New text",
          "name" : "text"
        }, {
          "kind" : "string",
          "description" : "\"HTML\", \"Markdown\" or \"MarkdownV2\"; plain text when left out",
          "name" : "parseMode"
        }, {
          "kind" : "table",
          "description" : "New buttons in the format of ask; nil removes the buttons",
          "name" : "buttons"
        } ]
      } ],
      "examples" : [ {
        "code" : "local message = bot.send(\"Run started\")\nbot.edit(message, \"Run finished: <b>OK</b>\", \"HTML\")"
      } ]
    }, {
      "name" : "waitCallback",
      "description" : "Waits until someone presses a button on the given message (a table returned by ask) and returns the callback_data of that button, or nil when the timeout passes first. The press is acknowledged to Telegram automatically (no need for ack). Presses on other messages are skipped. With a user id only that user's presses count; a press by anyone else is answered with a \"Not allowed\" notice and ignored. Without it anyone in the chat can answer. The buttons stay visible after the answer; use edit to remove them. A timeout of 0 returns nil without checking.",
      "returns" : {
        "kind" : "string",
        "description" : "The callback_data of the pressed button, or nil on timeout",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "shape",
          "ref" : "TelegramMessage",
          "description" : "Message table returned by ask",
          "name" : "message"
        }, {
          "kind" : "number",
          "description" : "Seconds to wait before giving up",
          "name" : "timeout"
        } ]
      }, {
        "params" : [ {
          "kind" : "shape",
          "ref" : "TelegramMessage",
          "description" : "Message table returned by ask",
          "name" : "message"
        }, {
          "kind" : "number",
          "description" : "Seconds to wait before giving up",
          "name" : "timeout"
        }, {
          "kind" : "number",
          "description" : "Telegram user id; only this user's button presses are accepted",
          "name" : "userId",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "local question = bot.ask(\"Continue?\", { { \"Yes\", \"yes\" }, { \"No\", \"no\" } })\nlocal answer = bot.waitCallback(question, 300)\nif answer == nil then\n    bot.edit(question, \"No answer, aborting\")\nend"
      } ]
    }, {
      "name" : "waitReply",
      "description" : "Waits for the next text message sent in the bot's chat and returns it, or nil when the timeout passes. Messages from other chats and messages without text (photos, stickers) are skipped. Messages that arrived before this call are ignored (they are marked as read), so an old message cannot be mistaken for the answer. With a user id only messages from that user count; anyone else in the chat is ignored.",
      "returns" : {
        "kind" : "shape",
        "ref" : "TelegramReply",
        "nullable" : true
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Seconds to wait before giving up",
          "name" : "timeout"
        } ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Seconds to wait before giving up",
          "name" : "timeout"
        }, {
          "kind" : "number",
          "description" : "Telegram user id; only messages from this user are accepted",
          "name" : "userId",
          "optional" : true
        } ]
      } ],
      "examples" : [ {
        "code" : "bot.send(\"Which environment should I test? (reply with a name)\")\nlocal reply = bot.waitReply(300)\nif reply then\n    print(\"Testing \" .. reply.text .. \" as requested by \" .. (reply.from and reply.from.username or \"?\"))\nend"
      } ]
    }, {
      "name" : "ack",
      "description" : "Answers a button press so the button stops showing a loading spinner, optionally with a short notification text. Only needed for callbacks you read yourself with poll(); waitCallback already does it.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "The id field of the callback query",
          "name" : "callbackId"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The id field of the callback query",
          "name" : "callbackId"
        }, {
          "kind" : "string",
          "description" : "Short text shown to the user",
          "name" : "text"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "The id field of the callback query",
          "name" : "callbackId"
        }, {
          "kind" : "string",
          "description" : "Short text shown to the user",
          "name" : "text"
        }, {
          "kind" : "boolean",
          "description" : "true for a popup the user must dismiss, false for a brief toast",
          "name" : "showAlert"
        } ]
      } ],
      "examples" : [ {
        "code" : "for _, update in ipairs(bot.poll()) do\n    if update.callback_query then\n        bot.ack(update.callback_query.id, \"Got it\")\n    end\nend"
      } ]
    }, {
      "name" : "poll",
      "description" : "Fetches the new updates the bot has received (messages and button presses from any chat, in order) and marks them as read, so each update is returned once. It waits up to the long polling time (30 seconds unless changed with pollTimeout) when there is nothing new, and returns an empty list then. Use it to find a chat id or to discard old messages; waitReply and waitCallback are easier for conversations. Messages in updates have no sender information.",
      "returns" : {
        "kind" : "list",
        "ref" : "TelegramUpdate"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "bot.pollTimeout(5)\nfor _, update in ipairs(bot.poll()) do\n    if update.message then\n        print(\"chat id \" .. update.message.chat_id .. \": \" .. update.message.text)\n    end\nend"
      } ]
    }, {
      "name" : "pollTimeout",
      "description" : "Sets how long poll() and the wait functions ask Telegram to hold a request open when nothing new has arrived, in seconds (default 30). waitReply and waitCallback never wait longer than their own timeout. Raises an error unless the value is from 1 to 60.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Seconds, from 1 to 60",
          "name" : "seconds"
        } ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes the HTTP client of this bot. The bot cannot be used afterwards; create a new one with Telegram.bot.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Timer",
    "kind" : "library",
    "description" : "A stopwatch for measuring how long steps of a script take.\n\nThere is one timer per run. A background thread adds 1 ms to a counter every millisecond while the timer runs, so values are approximate and can lag when the agent is busy. The timer stops itself one hour after `Timer.start`. Read the time with `get`, `milliseconds`, `seconds`, `minutes` or `hours` before calling `Timer.stop`, because stopping also resets the counter to zero.",
    "library" : "Timer",
    "category" : "utils",
    "variable" : "Timer",
    "functions" : [ {
      "name" : "start",
      "description" : "Starts counting. Calling it while the timer runs keeps the current value and continues; after `Timer.stop` or `Timer.reset` it counts from zero.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "Timer.start()\n-- ... the work you want to measure ...\nprint(\"took \" .. Timer.get())\nprint(Timer.milliseconds() >= 0)\nTimer.stop()",
        "runnable" : true
      } ]
    }, {
      "name" : "stop",
      "description" : "Stops counting and resets the counter to zero, so read the time before calling it. Does nothing when the timer is not running.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "reset",
      "description" : "Sets the counter back to zero without stopping it: a running timer keeps counting from zero.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "get",
      "description" : "Returns the counted time as text `HH:MM:SS:mmm` (hours, minutes, seconds, milliseconds), for example `00:01:05:250`.",
      "returns" : {
        "kind" : "string",
        "description" : "The time as `HH:MM:SS:mmm`"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "milliseconds",
      "description" : "Returns the counted time in milliseconds.",
      "returns" : {
        "kind" : "number",
        "description" : "Whole milliseconds"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "seconds",
      "description" : "Returns the counted time in seconds, with decimals.",
      "returns" : {
        "kind" : "number",
        "description" : "Seconds, for example 1.25"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "minutes",
      "description" : "Returns the counted time in minutes, with decimals.",
      "returns" : {
        "kind" : "number",
        "description" : "Minutes, with decimals"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "hours",
      "description" : "Returns the counted time in hours, with decimals.",
      "returns" : {
        "kind" : "number",
        "description" : "Hours, with decimals"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Xlsx",
    "kind" : "library",
    "description" : "Opens existing Excel `.xlsx` workbooks, reads and changes cells and saves the result.\n\nIt cannot create a workbook from scratch: start from an existing file or template. Only the `.xlsx` format is supported, and `.xlsx` is appended to a file name that does not end with it.\n\nRows and columns are numbered from 1 and the column comes before the row in `getCell` and `setCell`. Cells are read as text the way Excel shows them: formulas are evaluated, numbers use the number format of the cell, and dates become `yyyy-MM-dd` (or `yyyy-MM-dd HH:mm:ss` when they have a time), which `Xlsx.dateFormat` can change.",
    "library" : "Xlsx",
    "category" : "files",
    "variable" : "Xlsx",
    "functions" : [ {
      "name" : "open",
      "description" : "Opens an existing workbook. Backslashes in the path are turned into `/` and `.xlsx` is added when the name does not end with it. The workbook is loaded into memory: changes only reach disk with `Workbook.save`.\n\nThrows `Xlsx.open failed` when the file is missing or cannot be read, and the parser's own message when the file is not a valid `.xlsx`.",
      "returns" : {
        "kind" : "object",
        "ref" : "Workbook"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Workbook file, relative to the workspace",
          "name" : "location"
        } ]
      } ],
      "examples" : [ {
        "code" : "-- template.xlsx must already exist in the project; Xlsx edits existing workbooks\nlocal workbook = Xlsx.open(\"template.xlsx\")\nlocal sheet = workbook.sheet(\"Sheet1\")\nsheet.setCell(1, 2, \"Alice\")\nprint(sheet.getCell(1, 2))\nworkbook.save(\"report.xlsx\")\nworkbook.close()",
        "runnable" : true
      } ]
    }, {
      "name" : "dateFormat",
      "description" : "Sets the format used when date cells are read, for workbooks that are already open and for ones opened later. Without it dates are `yyyy-MM-dd`, or `yyyy-MM-dd HH:mm:ss` when the cell has a time.\n\nOnce set, the pattern is used for every date cell, with or without a time, so add `HH:mm:ss` if you need the time. Throws `Xlsx.dateFormat received an invalid date pattern` for a bad pattern.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Java date-time pattern, for example `dd-MM-yyyy` or `dd/MM/yyyy HH:mm`",
          "name" : "pattern"
        } ]
      } ],
      "examples" : [ {
        "code" : "Xlsx.dateFormat(\"dd-MM-yyyy\")\nlocal sheet = Xlsx.open(\"template.xlsx\").sheet(\"Sheet1\")\nprint(sheet.getCell(1, 1))",
        "runnable" : true
      } ]
    }, {
      "name" : "close",
      "description" : "Closes every workbook opened with `Xlsx.open` that is still open. Unsaved changes are lost. Throws after trying all of them when one fails to close.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Sheet",
    "kind" : "object",
    "description" : "One sheet of a workbook: read single cells, read whole columns grouped by header, and write cells.\n\nRows and columns are numbered from 1. Cell texts are what Excel would show: formulas are evaluated, numbers follow the number format of the cell and dates follow `Xlsx.dateFormat`.",
    "library" : "Xlsx",
    "category" : "files",
    "variable" : "sheet",
    "howToGet" : [ {
      "type" : "Xlsx",
      "function" : "open",
      "call" : "Xlsx.open(location)",
      "returns" : "Workbook"
    }, {
      "type" : "Workbook",
      "function" : "sheet",
      "call" : "workbook.sheet(sheetName)",
      "returns" : "Sheet"
    } ],
    "returnedBy" : [ {
      "type" : "Workbook",
      "function" : "sheet"
    } ],
    "functions" : [ {
      "name" : "headers",
      "description" : "Returns the data of the sheet grouped by column header, taking row 1 as the header row.\n\nThe result is a table keyed by header text. Each value is a list with one entry for every existing row below the header, and each entry is a table `{ value = text, row = number, col = number }` where `row` and `col` are the 1-based position in the sheet. Rows that do not exist in the file are skipped, so use `row` rather than the list position to know where a value came from.\n\nColumns with an empty header and everything to the right of the last non-empty header are left out. Values are trimmed, and a cell whose text starts with `#--` and ends with `--#` is returned as an empty string. An empty sheet gives an empty table.",
      "returns" : {
        "kind" : "table",
        "description" : "Table keyed by header text; each value is a list of `{ value, row, col }` tables"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local sheet = Xlsx.open(\"template.xlsx\").sheet(\"Sheet1\")\nsheet.setCell(1, 2, \"Alice\")\nsheet.setCell(2, 2, 30)\nlocal data = sheet.headers()\nprint(data.name[1].value, data.age[1].value, data.name[1].row)",
        "runnable" : true
      } ]
    }, {
      "name" : "columns",
      "description" : "Returns every column of the sheet including the first row, without treating any row as a header.\n\nThe result is a list of columns in sheet order. Each column is a list of `{ value = text, row = number, col = number }` tables, one for every existing row. Columns that have no cell in any row are skipped, so the list index can differ from the sheet's column number: use `col`. Values are trimmed, and a cell with text between `#--` and `--#` is returned as an empty string. An empty sheet gives an empty table.",
      "returns" : {
        "kind" : "table",
        "description" : "List of columns; each column is a list of `{ value, row, col }` tables"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "getCell",
      "description" : "Returns the text of one cell. The column comes first, then the row (`getCell(2, 5)` is cell B5). The text is not trimmed. Formulas are evaluated, booleans are `TRUE` or `FALSE` and a cell that does not exist gives an empty string.\n\nA column below 1 throws a Java error; a row below 1 gives an empty string.",
      "returns" : {
        "kind" : "string",
        "description" : "The cell text, or an empty string when the cell is empty"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Column number, starting at 1 (A = 1)",
          "name" : "column"
        }, {
          "kind" : "number",
          "description" : "Row number, starting at 1",
          "name" : "row"
        } ]
      } ],
      "examples" : [ {
        "code" : "local sheet = Xlsx.open(\"template.xlsx\").sheet(\"Sheet1\")\nprint(sheet.getCell(1, 1), sheet.getCell(2, 1), \"[\" .. sheet.getCell(9, 9) .. \"]\")",
        "runnable" : true
      } ]
    }, {
      "name" : "setCell",
      "description" : "Sets one cell, creating the row and cell when needed. The column comes first, then the row. The cell style (number or date format) is not changed.\n\nNumbers, and text that parses as a number (`\"42\"`, `\"1.5\"`, `\"007\"`, `\"5d\"`), are stored as a number, so `\"007\"` becomes `7`. Booleans are stored as boolean cells, `nil` clears the cell (it becomes empty) and anything else is stored as text; text starting with `=` is stored as text, not as a formula. Nothing reaches disk until `Workbook.save`. A column or row below 1 throws a Java error.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "number",
          "description" : "Column number, starting at 1 (A = 1)",
          "name" : "cellLoc"
        }, {
          "kind" : "number",
          "description" : "Row number, starting at 1",
          "name" : "rowLoc"
        }, {
          "kind" : "any",
          "description" : "Text, number or boolean to store; numeric text becomes a number, nil clears the cell",
          "name" : "value"
        } ]
      } ],
      "examples" : [ {
        "code" : "local sheet = Xlsx.open(\"template.xlsx\").sheet(\"Sheet1\")\nsheet.setCell(1, 2, \"Alice\").setCell(2, 2, 31)\nprint(sheet.getCell(1, 2), sheet.getCell(2, 2))",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Workbook",
    "kind" : "object",
    "description" : "An Excel workbook that is open in memory: get its sheets, change them and save the workbook. Nothing is written to disk before `save`.",
    "library" : "Xlsx",
    "category" : "files",
    "variable" : "workbook",
    "howToGet" : [ {
      "type" : "Xlsx",
      "function" : "open",
      "call" : "Xlsx.open(location)",
      "returns" : "Workbook"
    } ],
    "returnedBy" : [ {
      "type" : "Xlsx",
      "function" : "open"
    } ],
    "functions" : [ {
      "name" : "sheet",
      "description" : "Returns the sheet with the given name. Throws `Xlsx.sheet could not find sheet` when the workbook has no such sheet.",
      "returns" : {
        "kind" : "object",
        "ref" : "Sheet"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the sheet, as on its tab",
          "name" : "sheetName"
        } ]
      } ],
      "examples" : [ {
        "code" : "local workbook = Xlsx.open(\"template.xlsx\")\nlocal sheet = workbook.sheet(\"Sheet1\")\nprint(sheet.getCell(1, 1))\nworkbook.close()",
        "runnable" : true
      } ]
    }, {
      "name" : "save",
      "description" : "Writes the workbook to a file, replacing it if it exists. `.xlsx` is added when the name does not end with it. The folder must already exist (create it with `Fl.mkdir`); the call does not create folders.\n\nBy default all formulas are recalculated before writing, so cells that other cells depend on have current values when the file is opened. With the mode `no-evaluate` the formulas are not recalculated: their cached results may be stale, but unrelated invalid formulas cannot make the save fail. That mode keeps all formulas as they are, so it is not a way to clean an untrusted spreadsheet.\n\nThrows `Xlsx.save failed` when the file cannot be written (for example because it is open in Excel) and for an unknown mode. Saving does not close the workbook.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File to write, relative to the workspace",
          "name" : "fileLocation"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "File to write, relative to the workspace",
          "name" : "fileLocation"
        }, {
          "kind" : "string",
          "description" : "`evaluate` (default) recalculates formulas before writing, `no-evaluate` writes without recalculating. Case is ignored.",
          "name" : "saveMode"
        } ]
      } ]
    }, {
      "name" : "close",
      "description" : "Closes this workbook and frees its memory. Save first: unsaved changes are lost, and the workbook cannot be used afterwards.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Xml",
    "kind" : "library",
    "description" : "Reads, builds, transforms and writes XML documents.\n\n`Xml.read` (text), `Xml.open` (file) and `Xml.create` (empty) give a `Doc`. From a `Doc` you get `El` elements to read text and attributes, walk the tree and add content. Functions that take a namespace accept a plain URI (`urn:orders`) or a prefix and URI in one string separated by a comma and a space (`o, urn:orders`); elements are looked up by URI.\n\nLook-ups do not return `nil` for something that is not there: `child` of an unknown name returns an empty element that fails with a Java error as soon as you use it, so test with `#el.children(\"name\")` first. Files are read and written in the JVM default charset (UTF-8 on current Java versions).",
    "library" : "Xml",
    "category" : "files",
    "variable" : "Xml",
    "functions" : [ {
      "name" : "create",
      "description" : "Creates an empty document without a root element. Add the root with `Doc.element`, then build the tree with `El.element`, `El.attribute` and `El.text`.",
      "returns" : {
        "kind" : "object",
        "ref" : "Doc"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local doc = Xml.create()\nlocal order = doc.element(\"order\").attribute(\"id\", \"7\")\norder.element(\"item\").text(\"Widget\")\norder.element(\"item\").text(\"Gadget\")\nprint(doc.toString())",
        "runnable" : true
      } ]
    }, {
      "name" : "open",
      "description" : "Parses an XML file into a document. The file is read in the JVM default charset, whatever the XML declaration says. Throws `Failed to open XML file` when the file is missing or is not well-formed XML.",
      "returns" : {
        "kind" : "object",
        "ref" : "Doc"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "XML file to read, relative to the workspace",
          "name" : "fileName"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"orders.xml\", \"<orders><order id='1'/><order id='2'/></orders>\")\nlocal doc = Xml.open(\"orders.xml\")\nprint(#doc.root().children(\"order\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "read",
      "description" : "Parses XML text into a document. Throws `Failed to convert string to xml` when the text is empty or not well-formed.",
      "returns" : {
        "kind" : "object",
        "ref" : "Doc"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Well-formed XML text",
          "name" : "input"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Xml.read(\"<orders><order id='1'>Widget</order><order id='2'>Gadget</order></orders>\")\nlocal order = doc.root().child(\"order\")\nprint(order.attributeValue(\"id\"), order.value())",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "Doc",
    "kind" : "object",
    "description" : "An XML document: get its root element, build it up, convert it to text, transform it with XSLT and write it to a file.\n\nA document holds exactly one root element. Functions that transform it (`xslt`, `removeNamespaces`) return a new document and leave this one unchanged.",
    "library" : "Xml",
    "category" : "files",
    "variable" : "doc",
    "howToGet" : [ {
      "type" : "Xml",
      "function" : "create",
      "call" : "Xml.create()",
      "returns" : "Doc"
    } ],
    "returnedBy" : [ {
      "type" : "Xml",
      "function" : "create"
    }, {
      "type" : "Xml",
      "function" : "open"
    }, {
      "type" : "Xml",
      "function" : "read"
    }, {
      "type" : "Doc",
      "function" : "xslt"
    }, {
      "type" : "Doc",
      "function" : "removeNamespaces"
    } ],
    "functions" : [ {
      "name" : "root",
      "description" : "Returns the root element. Throws when the document has no root yet.",
      "returns" : {
        "kind" : "object",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "element",
      "description" : "Creates the root element of the document and returns it. A root that was already there is replaced, together with everything below it. To add a child to an element use `El.element`.\n\nA single namespace argument is either a URI (the element is in that default namespace) or a prefix and URI in one string separated by a comma and a space (`soap, http://schemas.xmlsoap.org/soap/envelope/`).",
      "returns" : {
        "kind" : "object",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the root element",
          "name" : "name"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the root element",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Namespace: a URI, or `prefix, uri`",
          "name" : "uri"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the root element",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Namespace prefix",
          "name" : "prefix"
        }, {
          "kind" : "string",
          "description" : "Namespace URI",
          "name" : "uri"
        } ]
      } ]
    }, {
      "name" : "toString",
      "description" : "Returns the document as indented XML text, starting with an XML declaration.",
      "returns" : {
        "kind" : "string",
        "description" : "The XML text"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "write",
      "description" : "Writes the document to a file as indented XML with an XML declaration, replacing the file. Missing parent directories in the workspace are created, and a bare file name like `doc.xml` works.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File to write, relative to the workspace",
          "name" : "fileName"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Xml.read(\"<greeting>hello</greeting>\")\ndoc.write(\"out/greeting.xml\")\nprint(Fl.read(\"out/greeting.xml\")[2])",
        "runnable" : true
      } ]
    }, {
      "name" : "xslt",
      "description" : "Applies an XSLT 1.0 stylesheet and returns the result as a new document; this document is not changed. The result of the transformation must itself be well-formed XML. Throws `Failed to remove namespaces from xml` (the text is shared with `removeNamespaces`) when the stylesheet or the transformation is invalid.",
      "returns" : {
        "kind" : "object",
        "ref" : "Doc"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "XSLT stylesheet as text",
          "name" : "xsl"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Xml.read(\"<items><item>1</item><item>2</item></items>\")\nlocal stylesheet = [[\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n  <xsl:output method=\"xml\"/>\n  <xsl:template match=\"/\"><count><xsl:value-of select=\"count(//item)\"/></count></xsl:template>\n</xsl:stylesheet>]]\nprint(doc.xslt(stylesheet).root().value())",
        "runnable" : true
      } ]
    }, {
      "name" : "removeNamespaces",
      "description" : "Returns a new document without namespaces, which makes values easy to read from SOAP responses. Elements keep only their local name, elements in the SOAP envelope namespace (`http://schemas.xmlsoap.org/soap/envelope/`, so Envelope, Header and Body) are replaced by their content, `xsi:nil` attributes are dropped and every other attribute is converted into a child element with the attribute's name and value. This document is not changed.",
      "returns" : {
        "kind" : "object",
        "ref" : "Doc"
      },
      "overloads" : [ {
        "params" : [ ]
      } ],
      "examples" : [ {
        "code" : "local response = Xml.read(\"<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' xmlns:n='urn:n'><s:Body><n:Result><n:total>42</n:total></n:Result></s:Body></s:Envelope>\")\nprint(response.removeNamespaces().root().child(\"total\").value())",
        "runnable" : true
      } ]
    } ]
  }, {
    "name" : "El",
    "kind" : "object",
    "description" : "An XML element: read its text and attributes, move to its children and parent, and add or remove content.\n\nEditing functions that return the element itself (`text`, `attribute`, `namespace`, `detach`) can be chained. Namespace arguments are a URI or a prefix and URI in one string (`o, urn:orders`).",
    "library" : "Xml",
    "category" : "files",
    "variable" : "el",
    "howToGet" : [ {
      "type" : "Xml",
      "function" : "create",
      "call" : "Xml.create()",
      "returns" : "Doc"
    }, {
      "type" : "Doc",
      "function" : "root",
      "call" : "doc.root()",
      "returns" : "El"
    } ],
    "returnedBy" : [ {
      "type" : "Doc",
      "function" : "root"
    }, {
      "type" : "Doc",
      "function" : "element"
    }, {
      "type" : "El",
      "function" : "root"
    }, {
      "type" : "El",
      "function" : "up"
    }, {
      "type" : "El",
      "function" : "child"
    }, {
      "type" : "El",
      "function" : "children"
    }, {
      "type" : "El",
      "function" : "element"
    } ],
    "functions" : [ {
      "name" : "root",
      "description" : "Returns the element you started from with `Doc.root` or `Doc.element`, the top of this tree.",
      "returns" : {
        "kind" : "object",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "up",
      "description" : "Returns the parent element, so you can step back after `child`, `children` or `element`. Only elements reached through those functions know their parent: on the element from `Doc.root` or `Doc.element` (the top of the tree) `up()` returns nil.\n\nWith a number the call climbs that many levels (`up(2)` is `up().up()`) and throws `Can't get upper element` when the top is reached before that. The number must be above 0.",
      "returns" : {
        "kind" : "object",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "number",
          "description" : "Number of levels to climb, above 0 (see description)",
          "name" : "amount"
        } ]
      } ]
    }, {
      "name" : "string",
      "description" : "Returns this element and everything below it as compact XML text (no indentation).",
      "returns" : {
        "kind" : "string",
        "description" : "The XML text of the element"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "value",
      "description" : "Returns the text of the element: its own text and that of all elements below it, without tags.",
      "returns" : {
        "kind" : "string",
        "description" : "The concatenated text content"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    }, {
      "name" : "attributeValue",
      "description" : "Returns the value of an attribute. An attribute that is not there gives an empty string, so a missing attribute cannot be told apart from an empty one.",
      "returns" : {
        "kind" : "string",
        "description" : "The attribute value, or an empty string when absent"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Attribute name",
          "name" : "name"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Attribute name",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Namespace of the attribute: a URI, or `prefix, uri`",
          "name" : "ns"
        } ]
      } ]
    }, {
      "name" : "child",
      "description" : "Returns the first child element with the given name. Without a namespace argument only children that are in no namespace match; give the namespace for namespaced children.\n\nReturns nil when there is no such child, so an optional child can be tested with `if child then`.",
      "returns" : {
        "kind" : "object",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the child element",
          "name" : "child"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the child element",
          "name" : "child"
        }, {
          "kind" : "string",
          "description" : "Namespace of the child: a URI, or `prefix, uri`",
          "name" : "ns"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Xml.read(\"<order><customer>Alice</customer><item>Widget</item></order>\")\nlocal order = doc.root()\nprint(order.child(\"customer\").value())\nprint(#order.children(\"discount\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "children",
      "description" : "Returns the child elements as a list, in document order: all children, or only those with the given name. The list is empty when there are none.\n\nWithout a namespace argument only children that are in no namespace match; pass the namespace to find namespaced children.",
      "returns" : {
        "kind" : "list",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the child elements",
          "name" : "child"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the child elements",
          "name" : "child"
        }, {
          "kind" : "string",
          "description" : "Namespace of the children: a URI, or `prefix, uri`",
          "name" : "ns"
        } ]
      } ],
      "examples" : [ {
        "code" : "local doc = Xml.read(\"<orders><order id='1'>A</order><order id='2'>B</order></orders>\")\nfor _, order in ipairs(doc.root().children(\"order\")) do\n    print(order.attributeValue(\"id\"), order.value())\nend",
        "runnable" : true
      } ]
    }, {
      "name" : "element",
      "description" : "Creates a new element, appends it as the last child of this element and returns it, so `text` and `attribute` can be chained on it. Use `up` to come back to this element.\n\nA single namespace argument is a URI or `prefix, uri`; the prefix and URI can also be passed as two arguments.",
      "returns" : {
        "kind" : "object",
        "ref" : "El"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the new element",
          "name" : "name"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the new element",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Namespace of the element: a URI, or `prefix, uri`",
          "name" : "ns"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the new element",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Namespace prefix",
          "name" : "prefix"
        }, {
          "kind" : "string",
          "description" : "Namespace URI",
          "name" : "uri"
        } ]
      } ]
    }, {
      "name" : "text",
      "description" : "Appends text to the element, after any text and children it already has. The text is escaped.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Text to append",
          "name" : "content"
        } ]
      } ]
    }, {
      "name" : "attribute",
      "description" : "Sets an attribute, replacing an attribute with the same name.\n\nWith a namespace the attribute is put in that namespace: pass `prefix, uri`, or just a URI, in which case a prefix that is already declared for the URI is used, or a new one (`ns1`, `ns2`, ...).",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the attribute",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Value of the attribute",
          "name" : "value"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Name of the attribute",
          "name" : "name"
        }, {
          "kind" : "string",
          "description" : "Value of the attribute",
          "name" : "value"
        }, {
          "kind" : "string",
          "description" : "Namespace of the attribute: a URI, or `prefix, uri`",
          "name" : "uri"
        } ]
      } ]
    }, {
      "name" : "namespace",
      "description" : "Declares a namespace on the element (an `xmlns` attribute). With one argument, a URI declares the default namespace and `prefix, uri` a prefixed one; with two arguments the prefix and URI are given separately. The declaration does not move the element itself into the namespace.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Namespace to declare: a URI, or `prefix, uri`",
          "name" : "ns"
        } ]
      }, {
        "params" : [ {
          "kind" : "string",
          "description" : "Namespace prefix",
          "name" : "prefix"
        }, {
          "kind" : "string",
          "description" : "Namespace URI",
          "name" : "uri"
        } ]
      } ]
    }, {
      "name" : "detach",
      "description" : "Removes the element from its parent. The detached element is returned and can still be read or added elsewhere.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ ]
      } ]
    } ]
  }, {
    "name" : "Yaml",
    "kind" : "library",
    "description" : "Reads, writes, encodes and decodes YAML.\n\nMappings and sequences become Lua tables, numbers and booleans become Lua numbers and booleans, and a YAML `null` becomes `nil`, so keys with a null value are absent from the table. Decoding is safe: tags that create Java objects are refused and recursive aliases are an error. Only the first YAML document of a text or file is supported.\n\nAn unquoted date such as `2024-03-05` is typed as a timestamp by YAML and comes back as a date string like `Tue Mar 05 01:00:00 CET 2024`, not as `2024-03-05`: put dates in quotes.\n\nWhen encoding, a table is written as a sequence if its keys are 1..n and as a mapping otherwise, in block style with two-space indentation. Mapping keys come out in Lua's table order.",
    "library" : "Yaml",
    "category" : "files",
    "variable" : "Yaml",
    "functions" : [ {
      "name" : "open",
      "description" : "Reads a YAML file and decodes it like `Yaml.decode`. Throws `Failed to read YAML file` when the file cannot be read and `Failed to decode YAML file` when it is not valid YAML or holds more than one document.",
      "returns" : {
        "kind" : "any",
        "description" : "The decoded value: a table for a mapping or sequence, otherwise a string, number, boolean or nil"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "YAML file to read, relative to the workspace",
          "name" : "file"
        } ]
      } ],
      "examples" : [ {
        "code" : "Yaml.write(\"settings.yaml\", { name = \"demo\", retries = 3 })\nlocal settings = Yaml.open(\"settings.yaml\")\nprint(settings.name, settings.retries)",
        "runnable" : true
      } ]
    }, {
      "name" : "decode",
      "description" : "Decodes YAML text. Empty text gives `nil`. Numbers become Lua numbers; quote a value (`\"007\"`) to keep it a string. Throws `Failed to decode YAML` with the parser message when the text is not valid.",
      "returns" : {
        "kind" : "any",
        "description" : "The decoded value: a table for a mapping or sequence, otherwise a string, number, boolean or nil"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "YAML text to decode",
          "name" : "yaml"
        } ]
      } ],
      "examples" : [ {
        "code" : "local config = Yaml.decode(\"name: demo\\nretries: 3\")\nprint(config.name, config.retries)\nprint(Yaml.encode({ enabled = true }))",
        "runnable" : true
      } ]
    }, {
      "name" : "encode",
      "description" : "Encodes a table, string, number, boolean or `nil` as YAML text. The text ends with a line break. Throws `Failed to encode YAML` for functions and other values YAML cannot hold, and for tables that contain themselves.",
      "returns" : {
        "kind" : "string",
        "description" : "The YAML text"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "any",
          "description" : "Value to encode, usually a table",
          "name" : "value"
        } ]
      } ]
    }, {
      "name" : "write",
      "description" : "Encodes a value like `Yaml.encode` and writes it to a file, replacing the file if it exists and creating missing directories.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "YAML file to write, relative to the workspace",
          "name" : "fileName"
        }, {
          "kind" : "any",
          "description" : "Value to encode, usually a table",
          "name" : "value"
        } ]
      } ]
    } ]
  }, {
    "name" : "Zip",
    "kind" : "library",
    "description" : "Creates and extracts `.zip` archives in the project workspace.\n\nArchiving a directory stores it under its own name (zipping `data` gives entries `data/...`), and extracting an archive never writes outside the destination directory. Symbolic links are stored as links and not followed. Password-protected archives are not supported.",
    "library" : "Zip",
    "category" : "compression",
    "variable" : "Zip",
    "functions" : [ {
      "name" : "create",
      "description" : "Zips a file or a whole directory. A file is stored under its file name; a directory is stored together with its own name as the top folder, so zipping `data` and extracting the result gives `data/...` again. The archive is replaced if it exists, its missing parent directories are created, and the archive itself is left out when it lies inside the source directory.\n\nThrows `Failed to create zip archive` (for example `Source path does not exist`). No partial archive is left at the destination after a failure.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "File or directory to zip, relative to the workspace",
          "name" : "source"
        }, {
          "kind" : "string",
          "description" : "Zip file to create, relative to the workspace",
          "name" : "destination"
        } ]
      } ],
      "examples" : [ {
        "code" : "Fl.write(\"hello.txt\", \"hello\")\nZip.create(\"hello.txt\", \"hello.zip\")\nZip.extract(\"hello.zip\", \"unzipped\")\nprint(Fl.exists(\"unzipped/hello.txt\"))",
        "runnable" : true
      } ]
    }, {
      "name" : "extract",
      "description" : "Extracts a zip archive into a directory, creating the directory when needed. Existing files with the same name are overwritten.\n\nAn entry whose path would end up outside the destination (for example `../x`), or a symbolic link pointing outside it, aborts the extraction. Throws `Failed to extract zip archive` when the archive is missing or not a zip file; entries extracted before an error stay on disk.",
      "returns" : {
        "kind" : "self"
      },
      "overloads" : [ {
        "params" : [ {
          "kind" : "string",
          "description" : "Zip file to extract, relative to the workspace",
          "name" : "archive"
        }, {
          "kind" : "string",
          "description" : "Directory to extract into, relative to the workspace",
          "name" : "destination"
        } ]
      } ]
    } ]
  } ],
  "shapes" : [ {
    "name" : "AgentSessionResult",
    "description" : "The result of a message sent in a CLI agent session.",
    "fields" : [ {
      "kind" : "boolean",
      "description" : "true when the CLI exited with code 0",
      "name" : "success"
    }, {
      "kind" : "number",
      "description" : "process exit code",
      "name" : "exit"
    }, {
      "kind" : "string",
      "description" : "the answer of the agent",
      "name" : "text"
    }, {
      "kind" : "string",
      "description" : "the complete CLI output",
      "name" : "all"
    }, {
      "kind" : "number",
      "description" : "run time in milliseconds",
      "name" : "duration"
    }, {
      "kind" : "string",
      "description" : "id of the session, nil until the CLI reports it",
      "name" : "sessionId"
    }, {
      "kind" : "list",
      "description" : "the answer messages as a list of strings",
      "name" : "messages"
    } ]
  }, {
    "name" : "AgentTaskResult",
    "description" : "The result of a one-off task run with a CLI agent library (Codex, Copilot, OpenCode).",
    "fields" : [ {
      "kind" : "boolean",
      "description" : "true when the CLI exited with code 0",
      "name" : "success"
    }, {
      "kind" : "number",
      "description" : "process exit code",
      "name" : "exit"
    }, {
      "kind" : "string",
      "description" : "the final answer of the agent",
      "name" : "text"
    }, {
      "kind" : "string",
      "description" : "the complete CLI output",
      "name" : "all"
    }, {
      "kind" : "number",
      "description" : "run time in milliseconds",
      "name" : "duration"
    } ]
  }, {
    "name" : "CouchResponse",
    "description" : "The response of a CouchDB request made with Couch. A non-2xx status is not an error: CouchDB reports problems in the body ({\"error\": \"not_found\", \"reason\": \"missing\"}), so check status yourself.",
    "fields" : [ {
      "kind" : "number",
      "description" : "HTTP status code, for example 200, 201 or 404",
      "name" : "status"
    }, {
      "kind" : "string",
      "description" : "the response body as pretty-printed JSON text",
      "name" : "json"
    }, {
      "kind" : "table",
      "description" : "the response body decoded into a Lua table (the same data as json)",
      "name" : "table"
    } ]
  }, {
    "name" : "DashboardTextOptions",
    "description" : "A piece of dashboard text and how to show it. Only value is required. Headers ignore size (their size is the second argument of Dashboard.h).",
    "fields" : [ {
      "kind" : "string",
      "description" : "the text to show",
      "name" : "value"
    }, {
      "kind" : "string",
      "description" : "text color as a CSS color, for example red or #ff8800; default is the page color",
      "name" : "color",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "background color as a CSS color; default is none",
      "name" : "background",
      "optional" : true
    }, {
      "kind" : "number",
      "description" : "font size in pixels, a number; default is the page font size",
      "name" : "size",
      "optional" : true
    } ]
  }, {
    "name" : "DownloadResponse",
    "description" : "The result of Rest.download: the response metadata, with the body saved to a file. The status is not checked, so a 404 or 500 body is saved too.",
    "fields" : [ {
      "kind" : "number",
      "description" : "HTTP status code, for example 200 or 404",
      "name" : "status"
    }, {
      "kind" : "string",
      "description" : "path of the saved file, relative to the project, with forward slashes",
      "name" : "file"
    }, {
      "kind" : "number",
      "description" : "size of the saved file in bytes",
      "name" : "size"
    }, {
      "kind" : "string",
      "description" : "always an empty string (the body is in the file)",
      "name" : "body"
    }, {
      "kind" : "string",
      "description" : "always an empty string (the body is in the file)",
      "name" : "response"
    }, {
      "kind" : "table",
      "description" : "response headers by name. Each header is present under its original name and under its lower-case name. The value is a string, or a list of strings when the header occurred more than once",
      "name" : "headers"
    }, {
      "kind" : "list",
      "ref" : "RestCookie",
      "description" : "all cookies in the Rest cookie jar after this response",
      "name" : "cookies"
    } ]
  }, {
    "name" : "FileStat",
    "description" : "Metadata of a file or directory, returned by Fl.stat. For a path that does not exist only path and exists (false) are set.",
    "fields" : [ {
      "kind" : "string",
      "description" : "the path relative to the workspace, with forward slashes (the workspace itself is .)",
      "name" : "path"
    }, {
      "kind" : "boolean",
      "description" : "whether the path exists",
      "name" : "exists"
    }, {
      "kind" : "string",
      "description" : "the last part of the path",
      "name" : "name",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "file, directory or other (for example a device)",
      "name" : "type",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "true for a regular file",
      "name" : "isFile",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "true for a directory",
      "name" : "isDirectory",
      "optional" : true
    }, {
      "kind" : "number",
      "description" : "size in bytes (for a directory this is whatever the file system reports, not the size of its content)",
      "name" : "size",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "last modification time as an ISO 8601 instant in UTC, for example 2026-01-31T12:30:00.123Z",
      "name" : "modifiedAt",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "creation time as an ISO 8601 instant in UTC; some file systems report the modification time",
      "name" : "createdAt",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "last access time as an ISO 8601 instant in UTC",
      "name" : "accessedAt",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "the path of the containing directory relative to the workspace; left out when it lies outside the workspace",
      "name" : "parent",
      "optional" : true
    } ]
  }, {
    "name" : "HttpResponse",
    "description" : "The response of an HTTP request made with Rest. A non-2xx status is not an error: check status yourself.",
    "fields" : [ {
      "kind" : "number",
      "description" : "HTTP status code, for example 200 or 404",
      "name" : "status"
    }, {
      "kind" : "string",
      "description" : "response body decoded as UTF-8 text, empty when the response has no body",
      "name" : "body"
    }, {
      "kind" : "string",
      "description" : "same value as body (older scripts use this name)",
      "name" : "response"
    }, {
      "kind" : "table",
      "description" : "response headers by name. Each header is present under its original name and under its lower-case name (headers[\"content-type\"]). The value is a string, or a list of strings when the header occurred more than once (such as Set-Cookie)",
      "name" : "headers"
    }, {
      "kind" : "list",
      "ref" : "RestCookie",
      "description" : "all cookies in the Rest cookie jar after this response, not only those set by this response",
      "name" : "cookies"
    } ]
  }, {
    "name" : "MailAttachment",
    "description" : "Metadata of an attachment of a mail message. Inline images that are referenced by the HTML body (they have a Content-ID) are not counted as attachments.",
    "fields" : [ {
      "kind" : "string",
      "description" : "attachment file name, decoded; empty when the part has none",
      "name" : "filename"
    }, {
      "kind" : "string",
      "description" : "MIME content type, for example application/pdf; name=\"invoice.pdf\"",
      "name" : "content_type"
    }, {
      "kind" : "number",
      "description" : "size in bytes as reported by the server, 0 when unknown",
      "name" : "size"
    } ]
  }, {
    "name" : "MailboxSettings",
    "description" : "Connection settings for Mail.imap and Mail.pop. Any other key of the table is passed to JavaMail as a property, so you can add for example [\"mail.imap.connectiontimeout\"] = 10000 or [\"mail.imap.ssl.trust\"] = \"mail.example.com\".",
    "fields" : [ {
      "kind" : "string",
      "description" : "mail server host name (required; also accepted as mail.imap.host or mail.pop3.host)",
      "name" : "host"
    }, {
      "kind" : "string",
      "description" : "user name to log in with (required; also accepted as username)",
      "name" : "user"
    }, {
      "kind" : "string",
      "description" : "password to log in with (required; use secret())",
      "name" : "password"
    }, {
      "kind" : "number",
      "description" : "server port. Defaults: IMAP 993 with SSL and 143 without, POP3 995 with SSL and 110 without",
      "name" : "port",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "connect over SSL/TLS from the start. Default: true for IMAP and for ports 993 and 995, false for POP3 and for ports 143 and 110",
      "name" : "ssl",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "upgrade a plain connection with STARTTLS; default false",
      "name" : "starttls",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "folder to open first; default INBOX (also accepted as mailbox). POP3 only has INBOX",
      "name" : "folder",
      "optional" : true
    } ]
  }, {
    "name" : "MailMessage",
    "description" : "A message read from a mailbox. Text fields that the message does not have are empty strings, never nil. Reading a message does not change it on the server (the mailbox is opened read-only), so seen is not set by reading.",
    "fields" : [ {
      "kind" : "number",
      "description" : "sequence number in the folder (1 is the oldest message); use it with Mailbox.get and Mailbox.downloadAttachments. It can change when messages are added or removed, so do not store it",
      "name" : "number"
    }, {
      "kind" : "string",
      "description" : "name of the folder the message was read from",
      "name" : "folder"
    }, {
      "kind" : "string",
      "description" : "subject line, decoded",
      "name" : "subject"
    }, {
      "kind" : "string",
      "description" : "value of the Message-ID header",
      "name" : "message_id"
    }, {
      "kind" : "list",
      "description" : "sender addresses, each as the server gives it: a display name followed by the address in angle brackets, or just the address (alice@example.com)",
      "name" : "from"
    }, {
      "kind" : "list",
      "description" : "recipients in the To header, formatted like from",
      "name" : "to"
    }, {
      "kind" : "list",
      "description" : "recipients in the Cc header",
      "name" : "cc"
    }, {
      "kind" : "list",
      "description" : "recipients in the Bcc header; normally empty because senders' Bcc headers are not kept",
      "name" : "bcc"
    }, {
      "kind" : "list",
      "description" : "addresses replies should go to",
      "name" : "reply_to"
    }, {
      "kind" : "number",
      "description" : "Date header as a Unix timestamp in milliseconds, 0 when missing",
      "name" : "sent_at"
    }, {
      "kind" : "string",
      "description" : "Date header as an ISO 8601 UTC time such as 2025-03-01T08:30:00Z, empty when missing",
      "name" : "sent_at_iso"
    }, {
      "kind" : "number",
      "description" : "time the server received the message, in Unix milliseconds, 0 when unknown",
      "name" : "received_at"
    }, {
      "kind" : "string",
      "description" : "time the server received the message as ISO 8601 UTC, empty when unknown",
      "name" : "received_at_iso"
    }, {
      "kind" : "number",
      "description" : "message size in bytes as reported by the server, 0 when unknown",
      "name" : "size"
    }, {
      "kind" : "string",
      "description" : "MIME type of the whole message, for example multipart/mixed; boundary=...",
      "name" : "content_type"
    }, {
      "kind" : "string",
      "description" : "the plain text of the message; when it has no plain text part, the HTML source of the HTML part (not converted to text)",
      "name" : "body"
    }, {
      "kind" : "string",
      "description" : "all text/plain parts joined, empty when there is none",
      "name" : "text"
    }, {
      "kind" : "string",
      "description" : "all text/html parts joined, empty when there is none",
      "name" : "html"
    }, {
      "kind" : "list",
      "ref" : "MailAttachment",
      "description" : "metadata of the attachments (use Mailbox.downloadAttachments to save the files)",
      "name" : "attachments"
    }, {
      "kind" : "boolean",
      "description" : "true when attachments is not empty",
      "name" : "has_attachments"
    }, {
      "kind" : "boolean",
      "description" : "the message has been read (always false on POP3, which has no flags)",
      "name" : "seen"
    }, {
      "kind" : "boolean",
      "description" : "the message has been answered",
      "name" : "answered"
    }, {
      "kind" : "boolean",
      "description" : "the message is marked as deleted",
      "name" : "deleted"
    }, {
      "kind" : "boolean",
      "description" : "the message is a draft",
      "name" : "draft"
    }, {
      "kind" : "boolean",
      "description" : "the message is flagged",
      "name" : "flagged"
    }, {
      "kind" : "boolean",
      "description" : "the message arrived since the last session (IMAP only)",
      "name" : "recent"
    } ]
  }, {
    "name" : "MailSearch",
    "description" : "Search criteria for Mailbox.search and Mailbox.poll. Every key is optional and all given criteria must match. Text criteria are \"contains\" matches, ignoring case; they are not regular expressions. The search runs in the agent on messages it has downloaded, not on the server.",
    "fields" : [ {
      "kind" : "string",
      "description" : "text that must appear in the subject, sender, To recipients, body or Message-ID",
      "name" : "query",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "text that must appear in the subject",
      "name" : "subject",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "text that must appear in the sender, including the display name (searching for alice finds a sender with the display name Alice Smith)",
      "name" : "from",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "text that must appear in the To recipients (Cc and Bcc are not searched)",
      "name" : "to",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "text that must appear in the body (the plain text, or the HTML source when there is no plain text)",
      "name" : "body",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "text that must appear in the Message-ID header (also accepted as message_id)",
      "name" : "messageId",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "true for only read messages, false for only unread ones (always unread on POP3)",
      "name" : "seen",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "the opposite of seen; ignored when seen is given",
      "name" : "unseen",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "true for only messages with attachments, false for only messages without (also accepted as has_attachments)",
      "name" : "hasAttachments",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "only messages received on or after this moment. A date or date-time string (2025-03-01, 2025-03-01T08:00:00, 2025-03-01T08:00:00Z; without zone the agent's time zone applies) or a Unix timestamp in seconds or milliseconds. Messages without a received date never match",
      "name" : "since",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "only messages received before this moment, same formats as since",
      "name" : "before",
      "optional" : true
    }, {
      "kind" : "number",
      "description" : "return at most this many messages; 0 or left out means no limit. Results are newest first",
      "name" : "limit",
      "optional" : true
    } ]
  }, {
    "name" : "RestCookie",
    "description" : "A cookie stored in the Rest session.",
    "fields" : [ {
      "kind" : "string",
      "description" : "cookie name",
      "name" : "name"
    }, {
      "kind" : "string",
      "description" : "cookie value",
      "name" : "value"
    }, {
      "kind" : "string",
      "description" : "domain the cookie applies to",
      "name" : "domain"
    }, {
      "kind" : "string",
      "description" : "path the cookie applies to",
      "name" : "path"
    }, {
      "kind" : "boolean",
      "description" : "only sent over HTTPS",
      "name" : "secure"
    }, {
      "kind" : "boolean",
      "description" : "not readable by page scripts",
      "name" : "httpOnly"
    }, {
      "kind" : "boolean",
      "description" : "the cookie has an expiry date",
      "name" : "persistent"
    }, {
      "kind" : "string",
      "description" : "expiry date in ISO 8601, nil for session cookies",
      "name" : "expiry"
    } ]
  }, {
    "name" : "SharepointItem",
    "description" : "A file or folder in a SharePoint document library.",
    "fields" : [ {
      "kind" : "string",
      "description" : "file or folder name",
      "name" : "name"
    }, {
      "kind" : "string",
      "description" : "path relative to the library root; pass it to download, move or delete",
      "name" : "path"
    }, {
      "kind" : "boolean",
      "description" : "true for a folder, false for a file",
      "name" : "folder"
    }, {
      "kind" : "number",
      "description" : "size in bytes",
      "name" : "size"
    }, {
      "kind" : "string",
      "description" : "browser URL of the item",
      "name" : "webUrl"
    }, {
      "kind" : "string",
      "description" : "last modified date in ISO 8601",
      "name" : "lastModified"
    } ]
  }, {
    "name" : "SmtpSettings",
    "description" : "SMTP settings for Mail.properties. Besides the short keys below, every other key of the table is handed to JavaMail as a property, so any mail.smtp.* property works, written as a quoted key (for example [\"mail.smtp.connectiontimeout\"] = 10000 or [\"mail.smtp.ssl.trust\"] = \"smtp.example.com\"). The short keys host, port, auth, starttls and ssl are aliases for mail.smtp.host, mail.smtp.port, mail.smtp.auth, mail.smtp.starttls.enable and mail.smtp.ssl.enable; when both forms are present the mail.smtp.* key wins. Timeouts ([\"mail.smtp.connectiontimeout\"] and [\"mail.smtp.timeout\"], in milliseconds) default to waiting forever. Values may be strings, numbers or booleans; they are converted to strings.",
    "fields" : [ {
      "kind" : "string",
      "description" : "SMTP server host name (alias for mail.smtp.host). Without it JavaMail tries localhost",
      "name" : "host",
      "optional" : true
    }, {
      "kind" : "number",
      "description" : "server port (alias for mail.smtp.port); JavaMail's default is 25",
      "name" : "port",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "log in with user and password (alias for mail.smtp.auth). Without it no login is attempted",
      "name" : "auth",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "upgrade the connection with STARTTLS, usually port 587 (alias for mail.smtp.starttls.enable)",
      "name" : "starttls",
      "optional" : true
    }, {
      "kind" : "boolean",
      "description" : "encrypt from the start, usually port 465 (alias for mail.smtp.ssl.enable)",
      "name" : "ssl",
      "optional" : true
    }, {
      "kind" : "string",
      "description" : "user name to log in with, also used as the sender address of every mail, so it should be a full address such as bot@example.com (also accepted as username or mail.smtp.user)",
      "name" : "user"
    }, {
      "kind" : "string",
      "description" : "password to log in with (also accepted as mail.smtp.password); use secret()",
      "name" : "password"
    } ]
  }, {
    "name" : "SshResult",
    "description" : "The outcome of a command run with session.run. A non-zero exit code is not an error: check exitCode yourself.",
    "fields" : [ {
      "kind" : "number",
      "description" : "exit code of the command; 0 means success, -1 when the server did not report one",
      "name" : "exitCode"
    }, {
      "kind" : "string",
      "description" : "everything the command wrote to standard output",
      "name" : "stdout"
    }, {
      "kind" : "string",
      "description" : "everything the command wrote to standard error",
      "name" : "stderr"
    } ]
  }, {
    "name" : "TelegramCallback",
    "description" : "A press on an inline keyboard button.",
    "fields" : [ {
      "kind" : "string",
      "description" : "callback query id (pass it to TelegramBot.ack)",
      "name" : "id"
    }, {
      "kind" : "string",
      "description" : "the callback_data of the pressed button",
      "name" : "data"
    }, {
      "kind" : "shape",
      "ref" : "TelegramUser",
      "description" : "the user who pressed the button",
      "name" : "from"
    }, {
      "kind" : "shape",
      "ref" : "TelegramMessage",
      "description" : "the message the button belongs to",
      "name" : "message"
    } ]
  }, {
    "name" : "TelegramMessage",
    "description" : "A message that was sent to or received from a Telegram chat.",
    "fields" : [ {
      "kind" : "number",
      "description" : "Telegram message id",
      "name" : "message_id"
    }, {
      "kind" : "number",
      "description" : "id of the chat the message belongs to",
      "name" : "chat_id"
    }, {
      "kind" : "string",
      "description" : "message text",
      "name" : "text"
    }, {
      "kind" : "number",
      "description" : "unix timestamp (seconds) when the message was sent",
      "name" : "date"
    } ]
  }, {
    "name" : "TelegramReply",
    "description" : "A text reply received in the chat.",
    "fields" : [ {
      "kind" : "number",
      "description" : "Telegram message id",
      "name" : "message_id"
    }, {
      "kind" : "number",
      "description" : "id of the chat the reply was sent in",
      "name" : "chat_id"
    }, {
      "kind" : "string",
      "description" : "reply text",
      "name" : "text"
    }, {
      "kind" : "shape",
      "ref" : "TelegramUser",
      "description" : "the sender, when known",
      "name" : "from"
    } ]
  }, {
    "name" : "TelegramUpdate",
    "description" : "One update from the Telegram bot API.",
    "fields" : [ {
      "kind" : "number",
      "description" : "update id",
      "name" : "update_id"
    }, {
      "kind" : "shape",
      "ref" : "TelegramMessage",
      "description" : "the new message, when the update is a message",
      "name" : "message"
    }, {
      "kind" : "shape",
      "ref" : "TelegramCallback",
      "description" : "the button press, when the update is a callback",
      "name" : "callback_query"
    } ]
  }, {
    "name" : "TelegramUser",
    "description" : "A Telegram user or bot.",
    "fields" : [ {
      "kind" : "number",
      "description" : "Telegram user id",
      "name" : "id"
    }, {
      "kind" : "string",
      "description" : "user name without the @",
      "name" : "username"
    }, {
      "kind" : "string",
      "description" : "first name (only present in the bot info returned by TelegramBot.me)",
      "name" : "first_name"
    } ]
  } ]
}
