README.md 78 KB

ArOZ Online JavaScript Gateway Interface (AGI)

AGI is the server-side JavaScript runtime used by ArozOS for module scripts (.agi / .js). Scripts run in an Otto VM with sandboxed access to ArozOS functions.

This document is updated to match the current AGI implementation in mod/agi/agi*.go.

Maintainer note — keep Terminal in sync The Terminal webapp ships an in-app API reference panel that is driven by a separate structured data file: src/web/Terminal/docs/api.json. Whenever this README is updated (new functions, changed signatures, new library, etc.) that file must also be updated to keep the in-app help accurate. The JSON mirrors this README's section structure — one object per library section, each with a functions array of { name, sig, desc, ret, example } entries.

AGI Version

  • Runtime version: 3.7 (AgiVersion in agi.go)

Quick Start

// Basic response
sendResp("Hello from AGI");

// JSON response
sendJSONResp({ ok: true, time: Date.now() });

// Load a library
if (requirelib("filelib")) {
    var files = filelib.glob("user:/Desktop/*");
    sendJSONResp(files);
}

Runtime Globals

System globals

  • BUILD_VERSION
  • INTERNAL_VERSION
  • LOADED_MODULES
  • LOADED_STORAGES
  • __FILE__
  • HTTP_RESP
  • HTTP_HEADER

User globals

  • USERNAME
  • USERICON
  • USERQUOTA_TOTAL
  • USERQUOTA_USED
  • USER_VROOTS
  • USER_MODULES

Detached-script globals (execd child)

  • BUILD_VERSION: Current system build version
  • INTERNAL_VERSION: Internal version number
  • LOADED_MODULES: Array of loaded system modules
  • LOADED_STORAGES: Array of available storage pools
  • __FILE__: Current script file path
  • HTTP_RESP: Response content (automatically set)
  • HTTP_HEADER: Response content type (automatically set)
  • USERNAME: Current user's username
  • USERICON: Current user's icon path
  • USERQUOTA_TOTAL: User's total storage quota
  • USERQUOTA_USED: User's used storage quota
  • USER_VROOTS: User's accessible virtual root paths
  • USER_MODULES: User's accessible modules
  • EXECUTION_ID: UUIDv4 that uniquely identifies this script invocation — useful for correlating log lines across concurrent executions
  • PARENT_DETACHED (true)
  • PARENT_PAYLOAD (string payload)

Path Rules

  • Use virtual paths such as user:/, tmp:/, extuuid:/....
  • Many library functions auto-resolve relative paths against the running script.
  • Permission checks are enforced (CanRead / CanWrite).

Core AGI Functions

Response functions

sendResp(content)

Sets HTTP_RESP.

sendResp("done");

echo(content)

Appends text to current HTTP_RESP.

echo("Hello ");
echo("World");

sendOK()

Sets response to ok.

sendOK();

sendJSONResp(objectOrJsonString)

Sets HTTP_HEADER = application/json and writes JSON response.

sendJSONResp({ success: true, items: [1, 2, 3] });

DB functions

newDBTableIfNotExists(tableName)

newDBTableIfNotExists("my_table");

DBTableExists(tableName)

if (DBTableExists("my_table")) sendOK();

writeDBItem(tableName, key, value)

writeDBItem("my_table", "theme", "dark");

readDBItem(tableName, key)

var v = readDBItem("my_table", "theme");

listDBTable(tableName)

Returns key-value object.

var kv = listDBTable("my_table");
sendJSONResp(kv);

deleteDBItem(tableName, key)

deleteDBItem("my_table", "theme");

dropDBTable(tableName)

dropDBTable("my_table");

Module and scheduling

registerModule(jsonConfigString)

Registers a module from JSON config.

registerModule(JSON.stringify({
    Name: "MyApp",
    Desc: "Example module",
    Group: "Utilities",
    IconPath: "icon.png",
    Version: "1.0",
    StartDir: "index.html",
    SupportFW: true,
    LaunchFWDir: "index.html"
}));

addNightlyTask(scriptPath)

Adds a valid AGI script to nightly task list.

addNightlyTask("MyApp/nightly.agi");

Utility and flow

includes(scriptName)

Loads and executes another script relative to current script directory.

includes("helpers.js");

delay(ms)

Sleeps for milliseconds.

delay(500);

exit()

Stops script execution.

if (!userIsAdmin()) exit();

execd(scriptName, payload)

Executes another script asynchronously.

execd("worker.agi", JSON.stringify({ job: "thumbs" }));

Deprecated (kept for compatibility)

  • requirepkg(...) -> deprecated in AGI v3
  • execpkg(...) -> deprecated in AGI v3
  • decodeVirtualPath(...) -> deprecated
  • decodeAbsoluteVirtualPath(...) -> deprecated
  • encodeRealPath(...) -> deprecated

User/permission functions

pathCanWrite(vpath)

if (pathCanWrite("user:/Documents")) sendOK();

getUserPermissionGroup()

Returns JSON string.

var group = JSON.parse(getUserPermissionGroup());

userIsAdmin()

if (!userIsAdmin()) sendResp("admin only");

userExists(username) (admin only)

if (userExists("alice")) echo("exists");

createUser(username, password, defaultGroup) (admin only)

createUser("alice", "StrongPass", "default");

removeUser(username) (admin only)

removeUser("alice");

editUser(...)

Currently stubbed and always returns false.

Loading Libraries

Use requirelib(libName).

requirelib("filelib");

Registered library IDs:

  • filelib
  • imagelib
  • http
  • share
  • iot
  • appdata
  • sysinfo
  • ziplib (includes 7z support via ziplib.extract7zFile, ziplib.list7zFileContents, etc.)
  • sqlite (SQLite database access — not available on linux/mipsle or windows/arm/386)
  • llm (OpenAI / Anthropic LLM chat: text & file based, with pricing & quota)
  • cnn (CXNNAIO vision inference: classification, detection, segmentation, pose, oriented detection, face analysis)
  • sharedspace (multi-user collaboration spaces: texts / images / files / documents, ACLs, persistence)
  • meetroom (MeetRoom control: create / end meetings, attendance - requires MeetRoom module access)
  • office (ArozOS Office suite: .pptx / .xlsx / .docx converters + native zip container pack/unpack)
  • notification (raise notifications to users via the core notification system, with priority - requires the host to wire in a notification sender)
  • git (version control for folders in the user's file system: clone / status / stage / commit / branch / diff / fetch / pull / push, with encrypted per-user HTTPS credentials — requires the host to wire in a git manager)
  • ffmpeg (only when ffmpeg exists on host)

Special case:

  • requirelib("websocket") is injected only in HTTP request context.

filelib API

Load:

requirelib("filelib");

filelib.writeFile(vpath, content)

filelib.writeFile("user:/notes.txt", "hello");

filelib.readFile(vpath)

var t = filelib.readFile("user:/notes.txt");

filelib.deleteFile(vpath)

filelib.deleteFile("user:/notes.txt");

filelib.walk(vpath, mode)

mode: all, file, folder

var allFiles = filelib.walk("user:/", "file");

filelib.glob(pattern, sortMode)

sortMode supports default and user modes from FS sort settings.

var list = filelib.glob("user:/Desktop/*.jpg", "default");

filelib.aglob(pattern, sortMode)

Advanced glob.

var list = filelib.aglob("user:/Desktop/**/*.png", "default");

filelib.readdir(vpath, sortMode)

Returns array of objects: {Filename, Filepath, Ext, Filesize, Modtime, IsDir}.

var entries = filelib.readdir("user:/Desktop", "default");

filelib.filesize(vpath)

var size = filelib.filesize("user:/movie.mp4");

filelib.fileExists(vpath)

if (filelib.fileExists("user:/a.txt")) sendOK();

filelib.isDir(vpath)

if (filelib.isDir("user:/Desktop")) sendOK();

filelib.mkdir(vpath)

filelib.mkdir("user:/newfolder");

filelib.md5(vpath)

var hash = filelib.md5("user:/a.txt");

filelib.mtime(vpath, parseToUnix)

parseToUnix=true returns Unix timestamp; otherwise formatted string.

var ts = filelib.mtime("user:/a.txt", true);

filelib.rootName(vpath)

Returns storage root display name.

var root = filelib.rootName("user:/Desktop/a.txt");

imagelib API

Load:

requirelib("imagelib");

imagelib.getImageDimension(vpath)

Returns [width, height].

var dim = imagelib.getImageDimension("user:/img.jpg");

imagelib.resizeImage(src, dest, width, height)

imagelib.resizeImage("user:/img.jpg", "user:/img_small.jpg", 800, 600);

imagelib.resizeImageBase64(src, width, height, format)

Returns data URL string.

var b64 = imagelib.resizeImageBase64("user:/img.jpg", 320, 240, "jpeg");

imagelib.cropImage(src, dest, x, y, width, height)

imagelib.cropImage("user:/img.jpg", "user:/crop.jpg", 10, 10, 200, 200);

imagelib.loadThumbString(vpath)

Returns cached thumbnail base64 string.

var thumb = imagelib.loadThumbString("user:/img.jpg");

imagelib.hasExif(vpath)

if (imagelib.hasExif("user:/img.jpg")) echo("has exif");

imagelib.getExif(vpath)

Returns JSON string.

var exif = JSON.parse(imagelib.getExif("user:/img.jpg"));

http API

Load:

requirelib("http");

http.request(options)

Curl-like request giving full control over the method, headers and body. Returns a response object {ok, status, statusText, headers, body, error} (error is a non-empty string when the request could not be completed; body is base64 when responseType is "base64").

Supported options:

Field Description
url Target URL (required)
method HTTP method, default GET (case-insensitive)
headers Object of request headers to set, e.g. {"Authorization":"Bearer x"}
body Raw text request body
json Object sent as a JSON body (sets Content-Type: application/json)
form Object sent as application/x-www-form-urlencoded
bodyBase64 Binary request body, base64 encoded (sets application/octet-stream)
contentType Override the Content-Type header
username / password HTTP basic auth credentials
timeout Timeout in seconds (0 / omitted = no timeout)
followRedirect Follow 3xx redirects (default true)
responseType "text" (default) or "base64" for binary responses

Body precedence when several are supplied: bodyBase64 > form > json > body.

var resp = http.request({
    url: "https://example.com/api",
    method: "POST",
    headers: {"Authorization": "Bearer TOKEN"},
    json: {a: 1, b: 2}
});
if (resp.ok){
    console.log(resp.status, resp.body);
}

Convenience helpers built on http.request (all return the response object): http.put(url, body, headers, contentType), http.patch(url, body, headers, contentType), http.delete(url, headers), http.postForm(url, formObject, headers) and http.postJSON(url, object, headers).

http.get(url, headers)

headers is optional. Without it, returns the body string (or null on error) for backward compatibility; with it, returns the response body string.

var body = http.get("https://example.com");
var body2 = http.get("https://example.com", {"Authorization": "Bearer x"});

http.post(url, body, headers, contentType)

headers and contentType are optional. Without them the body is sent as JSON (backward compatible); with them the given headers / content type are used. Returns the response body string.

var body = http.post("https://example.com/api", JSON.stringify({a:1}));

http.head(url, headerKey)

  • Without headerKey: returns JSON string of all headers.
  • With headerKey: returns JSON string of that header value.
var headers = JSON.parse(http.head("https://example.com"));

http.getCode(url)

Returns status code.

var code = http.getCode("https://example.com");

http.download(url, destDirVpath, filenameOptional)

Downloads into destination directory.

http.download("https://example.com/a.zip", "user:/Downloads", "a.zip");

http.getb64(url)

var raw = http.getb64("https://example.com/logo.png");

http.redirect(targetUrl, statusCode)

Default status code is 307 when omitted.

http.redirect("https://example.com/new", 302);

share API

Load:

requirelib("share");

share.shareFile(vpath, timeoutSec)

timeoutSec=0 means no auto-expire.

var uuid = share.shareFile("user:/report.pdf", 3600);

share.removeShare(shareUUID)

share.removeShare(uuid);

share.checkShareExists(shareUUID)

if (share.checkShareExists(uuid)) sendOK();

share.fileIsShared(vpath)

if (share.fileIsShared("user:/report.pdf")) sendOK();

share.getFileShareUUID(vpath)

var sid = share.getFileShareUUID("user:/report.pdf");

share.checkSharePermission(shareUUID)

var perm = share.checkSharePermission(uuid);

iot API

Load:

requirelib("iot");

iot.ready()

if (!iot.ready()) sendResp("iot unavailable");

iot.scan()

Returns scanned device array.

var devices = iot.scan();

iot.list()

Returns cached device array.

var devices = iot.list();

iot.connect(deviceId, username, password, token)

iot.connect("dev-1", "admin", "pass", "");

iot.status(deviceId)

Returns parsed status object.

var s = iot.status("dev-1");

iot.exec(deviceId, endpointName, payloadObject)

Returns parsed result object or false.

var resp = iot.exec("dev-1", "toggle", { value: true });

iot.disconnect(deviceId)

iot.disconnect("dev-1");

iot.iconTag(deviceId)

var tag = iot.iconTag("dev-1");

appdata API (read-only web root access)

Load:

requirelib("appdata");

appdata.readFile(relativePathFromWebRoot)

var conf = appdata.readFile("MyApp/config.json");

appdata.listDir(relativeDirFromWebRoot)

Returns relative path array.

var files = appdata.listDir("MyApp");

appdata.getModuleList()

Returns parsed module list array.

var mods = appdata.getModuleList();

sysinfo API

Load:

requirelib("sysinfo");

sysinfo.getCPUUsage()

Returns CPU usage percent.

var cpu = sysinfo.getCPUUsage();

sysinfo.getRAMUsage()

Returns {used, total, percent}.

var ram = sysinfo.getRAMUsage();

sysinfo.getNetworkUsage()

Returns {rxRate, txRate, rxTotal, txTotal} in bytes / bytes per second.

var net = sysinfo.getNetworkUsage();

sysinfo.getDiskInfo()

Returns logical disk info array.

var disks = sysinfo.getDiskInfo();

ziplib API

Load:

requirelib("ziplib");

ziplib.extractZipFile(src, destDir)

ziplib.extractZipFile("user:/a.zip", "user:/out/");

ziplib.createZipFile(sourcesArrayOrString, outputZip)

ziplib.createZipFile(["user:/a.txt", "user:/b.txt"], "user:/bundle.zip");

ziplib.createTarFile(sourcesArrayOrString, outputTar)

ziplib.createTarFile(["user:/folder"], "user:/bundle.tar");

ziplib.extractTarFile(srcTar, destDir)

ziplib.extractTarFile("user:/bundle.tar", "user:/out/");

ziplib.createTarGzFile(sourcesArrayOrString, outputTarGz)

ziplib.createTarGzFile(["user:/folder"], "user:/bundle.tar.gz");

ziplib.extractTarGzFile(srcTarGz, destDir)

ziplib.extractTarGzFile("user:/bundle.tar.gz", "user:/out/");

ziplib.createGzFile(srcFile, outputGz)

ziplib.createGzFile("user:/a.log", "user:/a.log.gz");

ziplib.extractGzFile(srcGz, outputFile)

ziplib.extractGzFile("user:/a.log.gz", "user:/a.log");

ziplib.isValidZipFile(vpath)

Checks whether archive format is recognizable.

var ok = ziplib.isValidZipFile("user:/a.zip");

ziplib.listZipFileContents(zipPath)

Returns JSON tree string.

var tree = JSON.parse(ziplib.listZipFileContents("user:/a.zip"));

ziplib.listZipFileDir(zipPath, dirPathInZip)

Returns immediate child names array.

var items = ziplib.listZipFileDir("user:/a.zip", "docs");

ziplib.getFileFromZip(zipPath, filePathInZip)

Extracts one file to tmp:/ and returns that virtual path.

var tmp = ziplib.getFileFromZip("user:/a.zip", "docs/readme.txt");

ziplib.getCompressFileType(vpath)

Returns one of zip, 7z, tar, tar.gz, gz, unknown.

var t = ziplib.getCompressFileType("user:/a.tgz");

ziplib.extractAnyFile(srcArchive, destDir)

Auto-detects format and extracts.

ziplib.extractAnyFile("user:/archive.any", "user:/out/");

ziplib.createAnyZipFile(sourcesArrayOrString, outputPath, format)

format: zip, tar, tar.gz (tgz), gz.

ziplib.createAnyZipFile(["user:/folder"], "user:/bundle.tar.gz", "tar.gz");

7z support (extensions on ziplib)

The following functions are registered alongside the standard ziplib functions and operate on .7z archives. Load ziplib with requirelib("ziplib") as normal.

ziplib.extract7zFile(src, destDir) → bool

Extracts all files from a 7z archive to destDir.

requirelib("ziplib");
ziplib.extract7zFile("user:/archive.7z", "user:/out/");

ziplib.list7zFileDir(src, dirPathIn7z) → string[]

Lists immediate children of a directory inside a 7z archive. Directories are returned with a trailing /.

requirelib("ziplib");
var items = ziplib.list7zFileDir("user:/archive.7z", "docs");

ziplib.list7zFileContents(src) → string (JSON tree)

Returns the full contents of a 7z archive as a JSON tree (same schema as listZipFileContents).

requirelib("ziplib");
var tree = JSON.parse(ziplib.list7zFileContents("user:/archive.7z"));

ziplib.getFileFrom7z(src, filePathIn7z) → string (vpath)

Extracts a single file from a 7z archive to tmp:/ and returns its virtual path.

requirelib("ziplib");
var tmp = ziplib.getFileFrom7z("user:/archive.7z", "docs/readme.txt");

ziplib.extractPartial7z(src, paths, destDir) → bool

Extracts selected files or folders from a 7z archive. paths may be a JS array or a JSON string array. For folder selections the parent prefix is stripped; for file selections the file is placed flat in destDir (matching extractPartialZip semantics).

requirelib("ziplib");
ziplib.extractPartial7z("user:/archive.7z", ["docs/", "README.md"], "user:/out/");

ziplib.get7zFileInfo(src) → string (JSON)

Returns metadata about a 7z archive: { fileCount, dirCount, totalUncompressedSize, totalCompressedSize }. totalCompressedSize is always 0 because 7z uses solid compression.

requirelib("ziplib");
var info = JSON.parse(ziplib.get7zFileInfo("user:/archive.7z"));
console.log(info.fileCount + " files, " + info.totalUncompressedSize + " bytes");

sqlite API

Load:

requirelib("sqlite");

Platform note: the sqlite library is not available on linux/mipsle, windows/arm, or windows/386 builds (excluded at compile time).

sqlite.open() returns a connection object. All SQL operations go through that object. Connections are automatically closed when the script exits — including when it throws or calls exit() — so a handle can never be leaked; you may also call db.close() explicitly to release it early.

Concurrency. Databases are opened in WAL mode with a 5 second busy timeout, so readers never block the writer and simultaneous requests queue for the write lock instead of failing. If you are writing many rows in a loop, wrap them in db.transaction() — a batch of N separate db.exec() calls costs N lock cycles and N fsyncs, while one transaction costs one of each.

sqlite.open(vpath) → db

Opens (or creates) a SQLite database file at the given virtual path. Throws a SQLiteError on failure.

requirelib("sqlite");
var db = sqlite.open("user:/.appdata/myapp/data.sqlite");

db.exec(sql, params){lastInsertId, rowsAffected}

Executes a statement that does not return rows (INSERT, UPDATE, DELETE, CREATE …). params is an optional JS array of bound values.

db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
db.exec("INSERT INTO notes (body) VALUES (?)", ["Hello world"]);

db.query(sql, params) → object[]

Executes a SELECT and returns all matching rows as an array of plain objects.

var rows = db.query("SELECT * FROM notes WHERE id > ?", [0]);
rows.forEach(function(r) { console.log(r.id, r.body); });

db.queryRow(sql, params) → object | null

Like db.query() but returns only the first row, or null if no rows matched.

var row = db.queryRow("SELECT * FROM notes WHERE id = ?", [1]);
if (row) sendJSONResp(row);

db.tables() → string[]

Returns the names of all user-created tables in the database.

var tables = db.tables();
sendJSONResp(tables);

db.schema(tableName) → object[]

Returns column metadata for the table as an array of { cid, name, type, notnull, dflt_value, pk } objects (from PRAGMA table_info).

var cols = db.schema("notes");
sendJSONResp(cols);

db.transaction(fn) → any

Runs fn inside a single write transaction, committing when it returns and rolling back if it throws. fn receives the connection object, and whatever it returns becomes the return value of db.transaction().

Use this whenever you write more than a couple of rows at once: it collapses N lock/fsync cycles into one, and keeps the database available to other requests for far longer.

db.transaction(function(tx) {
    for (var i = 0; i < items.length; i++) {
        tx.exec("INSERT INTO notes (body) VALUES (?)", [items[i]]);
    }
});

The transaction opens with BEGIN IMMEDIATE, so the busy timeout applies to acquiring the write lock rather than deadlocking partway through. Transactions cannot be nested — calling transaction() inside fn throws.

db.close() → bool

Closes the database connection and releases the handle.

db.close();

Full sqlite example

requirelib("sqlite");

var db = sqlite.open("user:/.appdata/myapp/tasks.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, done INTEGER DEFAULT 0)");

// Insert
var res = db.exec("INSERT INTO tasks (title) VALUES (?)", ["Buy milk"]);
console.log("new id:", res.lastInsertId);

// Bulk insert — one transaction instead of one per row
db.transaction(function(tx) {
    var titles = ["Walk dog", "Pay rent", "Call mum"];
    for (var i = 0; i < titles.length; i++) {
        tx.exec("INSERT INTO tasks (title) VALUES (?)", [titles[i]]);
    }
});

// Query
var pending = db.query("SELECT * FROM tasks WHERE done = 0");
sendJSONResp(pending);

db.close();

llm API

Load:

requirelib("llm");

The llm library connects to any OpenAI-compatible or Anthropic endpoint configured by an admin in System Settings > AI Integration > AI Model (the settings tab and its /system/aimodel/... routes kept their original "AI Model" name; only the requirelib identifier changed from aimodel to llm). Per-model pricing and an optional token/cost quota are also defined there. The wire-protocol logic (OpenAI / Anthropic request building and response parsing) lives in the standalone mod/aiservers/llm Go package.

llm.chat(prompt, options) → string

Sends a single-turn text prompt and returns the assistant's reply. options is an optional object (see Options below).

requirelib("llm");
var reply = llm.chat("What is the capital of France?");
sendResp(reply);

With a system prompt and model override:

requirelib("llm");
var reply = llm.chat("Summarise this in one sentence.", {
    system: "You are a concise summariser.",
    model:  "gpt-4o-mini"
});
sendResp(reply);

llm.chatWithFile(prompt, files, options) → string

Like llm.chat() but attaches one or more virtual-path files to the message. Images are sent as base64 vision parts; text files are inlined as labelled text. files may be a single vpath string or an array.

requirelib("llm");
var reply = llm.chatWithFile(
    "Describe what you see in this image.",
    "user:/Photos/holiday.jpg"
);
sendResp(reply);

llm.request(messages, options) → object

Low-level call. Accepts the full OpenAI-style messages array and returns the raw response object (including usage and choices).

requirelib("llm");
var resp = llm.request([
    { role: "system",    content: "You are helpful." },
    { role: "user",      content: "Hi!" },
    { role: "assistant", content: "Hello! How can I help?" },
    { role: "user",      content: "Tell me a joke." }
]);
sendResp(resp.choices[0].message.content);

When the model exposes its chain-of-thought, choices[0].message.reasoning_content carries that "thinking" text separately from the answer (content). It is populated from DeepSeek's reasoning_content, OpenRouter's reasoning or Anthropic thinking content blocks, and is an empty string for models that do not return reasoning.

llm.streamRequest(messages, options, onDelta) → object

Like llm.request() but streams the completion: onDelta is called for every incremental chunk as the model produces it, and the fully assembled response (same shape as llm.request, including usage) is returned when the stream ends. Each delta is { content, reasoning }content is new answer text and reasoning is new chain-of-thought text; either may be empty for a given chunk.

The callback runs on the script's own goroutine, so it is safe to relay chunks straight to a browser over websocket.send():

requirelib("llm");
requirelib("websocket");
websocket.upgrade(300);

var resp = llm.streamRequest([{ role: "user", content: "Explain gravity" }], {}, function(d){
    if (d.reasoning != "") websocket.send(JSON.stringify({ type: "reasoning", content: d.reasoning }));
    if (d.content   != "") websocket.send(JSON.stringify({ type: "delta",     content: d.content }));
});
websocket.send(JSON.stringify({ type: "done", usage: resp.usage }));

Token usage is recorded against the metrics/quota board exactly as for a non-streaming call.

llm.usage() → object

Returns accumulated token / cost metrics across all models.

requirelib("llm");
var u = llm.usage();
sendJSONResp(u);
// { totalTokens, totalCost, totalRequests, perModel: { ... }, currency, ... }

llm.models() → object

Returns the configured default model name and a list of models that have pricing entries defined in System Settings.

requirelib("llm");
var m = llm.models();
sendJSONResp(m);
// { default: "gpt-4o", models: ["gpt-4o", "gpt-4o-mini", ...] }

llm.listModels() → object

Queries the live endpoint for available models (does not consume tokens).

requirelib("llm");
var m = llm.listModels();
sendJSONResp(m.models);

llm.fileParts(files) → object[]

Converts virtual-path file(s) into OpenAI-style content parts that can be embedded in a messages array for llm.request(). Images become image_url data-URI parts; text files become text parts.

requirelib("llm");
var parts = llm.fileParts(["user:/report.txt"]);
var resp  = llm.request([
    { role: "user", content: parts }
]);
sendResp(resp.choices[0].message.content);

Options object

All functions that accept options support the following fields (all optional):

Field Type Description
model string Override the configured default model
system string System prompt (prepended as a system role message)
endpoint string Override the global endpoint URL
apikey string Override the global API key
apiFormat string Wire format: "openai" (default) or "anthropic"
temperature number Sampling temperature
max_tokens number Maximum tokens to generate

cnn API

Load:

requirelib("cnn");

The cnn library connects to an external CXNNAIO vision-inference server configured by an admin in System Settings > AI Integration > CNN Inference (endpoint, optional bearer token, request timeout). Every function reads its input image from a virtual file path (the calling user must have read access); the returned object is the server's own response envelope (object, model, created, image, timing_ms, data, ...), so it matches the CXNNAIO API documentation field-for-field.

cnn.classify(file, options) → object

Image classification (default model mobilenet-v2).

requirelib("cnn");
var r = cnn.classify("user:/Photos/cat.jpg", { top_k: 3 });
sendJSONResp(r.data); // [{ label, index, score }, ...]

cnn.detect(file, options) → object

Object detection (default model yolo11n).

requirelib("cnn");
var r = cnn.detect("user:/Photos/street.jpg", { score_threshold: 0.3, render: true });
sendJSONResp(r.data);              // [{ label, class_id, score, box:{x1,y1,x2,y2} }, ...]
// r.rendered_image is a data URI PNG when render:true was set

cnn.segment(file, options) → object

Instance segmentation (yolo11n-seg). Each item carries a per-instance, box-cropped mask (mask.data is a base64 PNG).

cnn.pose(file, options) → object

Pose estimation (yolo11n-pose), 17 COCO keypoints per detected person.

cnn.oriented(file, options) → object

Oriented/rotated-box detection (yolo11n-obb), intended for aerial/top-down imagery.

cnn.faceDetect(file, options) → object

Face detection (default model ultraface-rfb-320).

cnn.faceLandmarks(file, options) → object

98-point facial landmarks (pfld). Set options.cropped = true to treat the whole input image as one face crop instead of detecting faces first.

cnn.faceEmbedding(file, options) → object

L2-normalized 128-d face embedding vector(s) (mbv2facenet).

cnn.faceAttributes(file, options) → object

Gender attributes per face (gender-mbv2-0.35). Calls the server's /v1/faces/gender route (the upstream API doc names this endpoint /v1/faces/attributes, but the deployed server registers it as gender; the response object field reads "face.gender").

cnn.faceCompare(fileA, fileB, options) → object

Compares two face photos/crops and returns their cosine similarity. Does not support options.async (the server has no async variant for this endpoint).

requirelib("cnn");
var r = cnn.faceCompare("user:/a.jpg", "user:/b.jpg", { threshold: 0.5 });
sendResp(r.similarity + " - " + (r.same ? "same person" : "different"));

cnn.analyze(file, tasks, options) → object

Runs several tasks over one image in a single round trip. tasks is an array ("classify", "detect", "segment", "pose", "oriented", "faces", "landmarks", "attributes"); options carries an optional top-level render/async plus per-task parameter blocks keyed by task name.

requirelib("cnn");
var r = cnn.analyze("user:/group.jpg", ["detect", "faces"], {
    render: true,
    detect: { score_threshold: 0.3 }
});
sendJSONResp(r.results.detect.data);
document_rendered = r.rendered_image; // data URI PNG

cnn.job(id) → object

Polls an async job (see Async below). Returns { id, object, status, created, result, error } where status is one of "queued", "running", "succeeded" or "failed".

cnn.models() → object

Live model registry from the configured server: { object, data: [{ id, object, task, classes, input }, ...] }.

cnn.health() → object

Live server health: { status, version, models_loaded, sessions, uptime_s }.

Async jobs

Every single-image function (classify, detect, segment, pose, oriented, faceDetect, faceLandmarks, faceEmbedding, faceAttributes, analyze) accepts options.async = true. When set, the function returns immediately with a job object instead of blocking:

requirelib("cnn");
var job = cnn.detect("user:/big.jpg", { async: true });
while (job.status === "queued" || job.status === "running") {
    delay(500);
    job = cnn.job(job.id);
}
sendJSONResp(job.status === "succeeded" ? job.result : job.error);

Options object

All single-image functions accept the same options fields, using the server's own field names so they match the CXNNAIO API documentation directly (all optional):

Field Type Description
model string Override the server's default model for this task
score_threshold number Minimum confidence to keep (detect/seg/pose/oriented/faces)
nms_threshold number IoU suppression threshold (detect/seg/pose/oriented/faces)
top_k number Number of ranked results (classification)
max_results number Cap on returned items
render bool Also return an annotated PNG in rendered_image
cropped bool Treat the whole input image as one face crop (face endpoints)
async bool Submit as an async job instead of blocking (see Async jobs)

cnn.faceCompare uses its own options shape instead: model, threshold, a_cropped, b_cropped.

office API

Converters between the ArozOS Office suite webapps (src/web/Office/) and common office file formats. Backed by mod/office (pure Go, no external dependencies). Word (.docx) and Excel (.xlsx) helpers will join this library as the Docs and Sheets webapps mature.

Load:

requirelib("office");

office.pptxToPresentation(srcVpath)

Parse a PowerPoint .pptx file into the Slides document body schema (see src/web/Office/common/CONTRACT.md). Returns the body as a JSON string, or throws on failure. Embedded pictures are inlined as data: URLs; text boxes, preset shapes, connector lines and tables map to their Slides object types. Unsupported content (video, native charts, SmartArt) is skipped.

requirelib("office");
var bodyJson = office.pptxToPresentation("user:/Desktop/deck.pptx");
sendJSONResp('{"body":' + bodyJson + '}');

office.presentationToPptx(bodyJson, destVpath)

Build a .pptx from a serialized Slides body JSON string and write it to destVpath. Image objects must be inlined as data: URLs and chart objects should carry a client-rendered PNG in props.png (the Slides webapp does both automatically before calling). Video/audio objects are not embedded: each renders as a poster picture (the captured frame in props.png, or a generated placeholder) and the media files themselves are packed into a sidecar zip written next to the pptx as <dest basename>.zip. Returns true on success, or the sidecar zip's vpath (a string, also truthy) when one was written.

requirelib("office");
var r = office.presentationToPptx(data, "user:/Desktop/out.pptx");
if (r){
    // r === "user:/Desktop/out.zip" when the deck had video/audio
    sendResp("OK");
}

office.xlsxToWorkbook(srcVpath)

Parse an Excel .xlsx file into the Sheets document body schema. Returns the body as a JSON string, or throws on failure. Handles values, formulas (recalculated by the webapp), shared/inline strings, cell styles, number formats, column widths / row heights, merged cells and frozen panes. Charts, pivot tables and conditional formatting are skipped. Legacy binary .xls is rejected with a message asking for .xlsx.

requirelib("office");
var bodyJson = office.xlsxToWorkbook("user:/Desktop/report.xlsx");
sendJSONResp('{"body":' + bodyJson + '}');

office.workbookToXlsx(bodyJson, destVpath)

Build a .xlsx from a serialized Sheets body JSON string and write it to destVpath. Returns true on success. Formulas are written natively so Excel recalculates them; webapp charts and filters are not exported.

requirelib("office");
if (office.workbookToXlsx(data, "user:/Desktop/out.xlsx")){
    sendResp("OK");
}

office.docxToDocument(srcVpath)

Parse a Word .docx file into the Docs document body schema. Returns the body as a JSON string, or throws on failure. Handles paragraphs, heading/title styles, alignment, inline formatting (bold/italic/underline/ strikethrough, color, size), hyperlinks, lists, tables, embedded images (inlined as data: URLs), header/footer text and page geometry. Tracked changes, footnotes and text boxes are ignored. Legacy binary .doc is rejected with a message asking for .docx.

requirelib("office");
var bodyJson = office.docxToDocument("user:/Desktop/report.docx");
sendJSONResp('{"body":' + bodyJson + '}');

office.documentToDocx(bodyJson, destVpath)

Build a .docx from a serialized Docs body JSON string and write it to destVpath. Returns true on success. Images must be inlined as data: URLs (the Docs webapp does this automatically before calling).

requirelib("office");
if (office.documentToDocx(data, "user:/Desktop/out.docx")){
    sendResp("OK");
}

office.packToFile(envelopeJson, destVpath)

Write an Office suite native file (.doca / .xlsa / .ppta) as a zip container: document.json plus deduplicated binary assets/. Media data URLs and legacy media?file= links inside the envelope become embedded assets, so the file stays portable when copied to another machine. Returns true on success.

office.unpackFromFile(srcVpath)

Read a native Office suite file and return its envelope JSON string with embedded assets re-inlined as data: URLs. Legacy plain-JSON documents pass through unchanged, so old files keep opening (and are upgraded to the container format on their next save).

office.unpackToWorkdir(srcVpath, workdirBase)

Read a native Office suite container and return its envelope JSON string with binary assets extracted into <workdirBase>/<doc-hash>/ and referenced by media?file= links instead of inline base64 - so the JSON stays small even for video-heavy documents (the Office webapps use user:/.appdata/Office/cache as the working directory). Legacy plain-JSON documents pass through unchanged.

requirelib("office");
var envelope = office.unpackToWorkdir("user:/Documents/deck.ppta", "user:/.appdata/Office/cache");
sendJSONResp('{"envelope":' + envelope + '}');

office.odtToDocument(srcVpath)

Read an OpenDocument Text file (.odt) and return the Docs body schema as a JSON string (headings, inline formatting, links, lists, tables with column widths and cell shading, embedded pictures as data: URLs, page geometry, header/footer and page breaks).

office.documentToOdt(jsonStr, destVpath)

Build an .odt from a serialized Docs body JSON and write it to destVpath. Covers the same subset as the docx exporter. Returns true on success.

office.odsToWorkbook(srcVpath)

Read an OpenDocument Spreadsheet (.ods) and return the Sheets body schema as a JSON string. Formulas are translated from the ODF of:=SUM([.A1:.B2]) syntax back to plain =SUM(A1:B2) references; cell styles, column widths, row heights, merges and cell notes (office:annotation) are kept.

office.workbookToOds(jsonStr, destVpath)

Build an .ods from a serialized Sheets body JSON and write it to destVpath (formulas rewritten to the ODF syntax so LibreOffice recalculates them). Returns true on success.

office.odpToPresentation(srcVpath)

Read an OpenDocument Presentation (.odp) and return the Slides body schema as a JSON string, scaled into the 960x540 editor space (text boxes, images, basic shapes, lines, tables, slide backgrounds and speaker notes).

office.presentationToOdp(jsonStr, destVpath)

Build an .odp from a serialized Slides body JSON and write it to destVpath. Charts export through their client-side PNG raster (props.png), like the pptx exporter; video/audio objects are skipped. Returns true on success.

requirelib("office");
var bodyJson = office.odsToWorkbook("user:/Documents/report.ods");
sendJSONResp('{"body":' + bodyJson + '}');

office.documentToPdf(jsonStr, destVpath)

Build a PDF with real, selectable text (not a page raster) from a serialized Docs body JSON and write it to destVpath. Renders the same HTML subset as the docx exporter: headings/paragraph styles, inline bold/italic/underline/color/size/highlight, clickable links, lists, tables with column widths, cell shading and cell content (block text, lists and images inside cells), inline data-URL images, explicit page breaks, page size/orientation/margins, and header/footer text with optional page numbers. Core PDF fonts are Latin-1; characters outside that range are transliterated. Returns true on success.

office.workbookPrintToPdf(printJson, destVpath)

Build a real-text PDF from a Sheets print model (not the raw workbook JSON): {"sheets":[{"name","colW":[px],"rowH":[px],"rows": [[{"t","b","i","u","fc","bg","al"}]]}]} — formatted display strings plus print-relevant styles, computed by the web client (which owns formula evaluation). One A4-landscape section per sheet, columns scaled down to fit when the sheet is wider than the page. Returns true on success.

office.presentationToPdf(jsonStr, destVpath)

Build a real-text PDF from a serialized Slides body JSON: one page per slide at the deck's canvas size (960x540 default). Text boxes, shape captions and tables are selectable text; images and charts embed from their client-inlined data URLs; video/audio objects render their captured poster frame (props.png) or a generic placeholder. Returns true on success.

requirelib("office");
var ok = office.documentToPdf(bodyJsonString, "user:/Desktop/report.pdf");
if (ok) { sendResp("OK"); }

ffmpeg API

Load:

requirelib("ffmpeg");

Note: library exists only when host has ffmpeg installed.

ffmpeg.convert(input, output, compression)

Generic conversion.

ffmpeg.convert("user:/in.mov", "user:/out.mp4", 0);

ffmpeg.audioConvert(input, output, sampleRate, progressFile)

ffmpeg.audioConvert("user:/in.wav", "user:/out.mp3", 44100, "tmp:/audio_progress.json");

ffmpeg.imageConvert(input, output, scaleFactor, compressionRate, progressFile)

progressFile is optional. Image conversions have no timeline, so the file only reports 0 % and, on success, 100 % — but passing it makes the job cancellable.

ffmpeg.imageConvert("user:/in.png", "user:/out.jpg", 0.5, 80, "tmp:/image_progress.json");

ffmpeg.videoConvert(input, output, resolution, compressionRate, progressFile)

ffmpeg.videoConvert("user:/in.mp4", "user:/out.mp4", "720p", 55, "tmp:/video_progress.json");

ffmpeg.convertWithProgress(input, output, progressFile)

ffmpeg.convertWithProgress("user:/in.mp4", "user:/out.gif", "tmp:/conv_progress.json");

ffmpeg.cancel(progressFile)

Stops a conversion that is still running, identified by the progress file it was started with. Returns true when a running conversion was found and terminated, false when it already finished or was started without a progress file. The conversion call itself then returns false like any other failed conversion, so the caller decides how a cancelled job is recorded.

Because the conversion runs in its own request, the cancel call is made from a separate script execution while the conversion request is still open.

// in the request that starts the job
ffmpeg.videoConvert("user:/in.mp4", "user:/out.mkv", "", 38, "tmp:/job42.progress.json");

// in a later request, to stop it
var stopped = ffmpeg.cancel("tmp:/job42.progress.json");

websocket API

The websocket library upgrades the current HTTP connection to a WebSocket session. It is only available in script paths reached via a live HTTP request context (standard InterfaceHandler or token-handler routes — not execd children).

Load:

requirelib("websocket");

Note on delay() after upgradewebsocket.upgrade() replaces the global delay() with a message-pumping version. While the script sleeps inside delay(), any queued inbound frames are dispatched to websocket.onMessage (if set). delay() is therefore the natural yield point in event-driven loops. When onMessage is null the buffer is left untouched so that available() and read() can still see the frames.


websocket.upgrade(timeoutSec)bool

Upgrades the HTTP connection to WebSocket and starts the background frame reader. The connection is closed automatically after timeoutSec seconds of idle time (default 300). Also installs the message-pumping delay() override.

Returns false if the upgrade fails.

requirelib("websocket");
if (!websocket.upgrade(120)) exit();

websocket.send(text)bool

Sends a UTF-8 text frame to the client. Returns false if the connection is closed.

websocket.send("Hello from server");

websocket.read(timeoutMs?)string | null | false

Reads the next inbound message from the internal buffer.

Return value Meaning
string Message text
null timeoutMs elapsed with no message; connection still open
false Connection is closed

timeoutMs = 0 or omitted blocks indefinitely until a message arrives or the connection closes.

// Block until a message arrives or connection closes
var msg = websocket.read();

// Wait at most 5 s; returns null on timeout
var msg = websocket.read(5000);

if (msg === false) { /* connection closed */ }
if (msg === null)  { /* timed out, still open */ }

websocket.available()number

Returns the number of messages currently queued in the inbound buffer. Non-blocking — safe to call on every iteration of a tight loop.

if (websocket.available() > 0) {
    var msg = websocket.read();
}

websocket.isClosed()bool

Returns true when the WebSocket connection is no longer active.

while (!websocket.isClosed()) {
    websocket.send("tick");
    delay(1000);
}

websocket.onMessage

Assign a function(msg) callback to receive messages asynchronously. The handler fires inside delay() on the script's own goroutine — Otto-safe, no concurrent JS execution.

Message object properties:

Property Type Description
msg.data string Text payload
msg.timestamp number Arrival time (Unix milliseconds)
msg.type number Frame type: 1 = text, 2 = binary
websocket.onMessage = function(msg) {
    console.log("Received at " + msg.timestamp + " ms: " + msg.data);
};

Set back to null to stop receiving callbacks and leave messages in the buffer:

websocket.onMessage = null;

websocket.close()

Sends a normal-closure frame and closes the connection.

websocket.close();

Pattern 1 — blocking read with optional timeout

Simplest pattern. read(timeoutMs) returns null on timeout so the loop can send a keep-alive or do other work without blocking forever.

requirelib("websocket");
if (!websocket.upgrade(120)) exit();

websocket.send("Connected. Commands: echo <text> | stop");

while (true) {
    var msg = websocket.read(30000); // wait up to 30 s

    if (msg === false) break;        // remote side closed
    if (msg === null)  {             // 30-second idle timeout
        websocket.send("Still here.");
        continue;
    }

    msg = msg.trim();
    if (msg === "stop") {
        websocket.send("Bye!");
        break;
    } else if (msg.indexOf("echo ") === 0) {
        websocket.send(msg.slice(5));
    } else if (msg !== "") {
        websocket.send("Unknown command: '" + msg + "'");
    }
}

websocket.close();

Pattern 2 — available() polling (Arduino-style)

Use when you want to drain all queued frames in one shot each iteration, or when the main loop body does other work regardless of incoming messages.

onMessage must be null (the default) so that delay() does not consume frames behind your back.

requirelib("websocket");
if (!websocket.upgrade(120)) exit();

websocket.send("available() polling mode.");

while (true) {
    if (websocket.isClosed()) break;

    var n = websocket.available();
    if (n > 0) {
        // Drain all waiting frames without blocking
        for (var i = 0; i < n; i++) {
            var msg = websocket.read(); // data already queued, returns immediately
            if (msg === false) break;
            msg = msg.trim();
            if (msg === "stop") {
                websocket.send("Bye!");
                websocket.close();
                break;
            }
            websocket.send("Echo: " + msg);
        }
    } else {
        delay(500); // sleep; buffer is untouched because onMessage is null
    }
}

Pattern 3 — onMessage callback with delay() pump

Event-driven style. The callback fires inside delay() on the script goroutine. Use a shared variable to hand data from the callback to the main loop.

requirelib("websocket");
if (!websocket.upgrade(120)) exit();

websocket.send("onMessage mode. Commands: echo <text> | stop");

var lastMessage = "";

websocket.onMessage = function(msg) {
    // Runs on the script goroutine during delay() — safe to update shared state
    lastMessage = msg.data;
};

while (true) {
    if (lastMessage !== "") {
        var msg = lastMessage.trim();
        lastMessage = "";

        if (msg === "stop") {
            websocket.send("Bye!");
            break;
        } else if (msg.indexOf("echo ") === 0) {
            websocket.send(msg.slice(5));
        } else if (msg !== "") {
            websocket.send("Unknown command: '" + msg + "'");
        }
    }

    if (websocket.isClosed()) break;

    // delay() pumps the inbound channel and fires onMessage for each queued frame
    delay(100);
}

websocket.onMessage = null;
websocket.close();

Scheduler Library (scheduler)

Load with: requirelib("scheduler")

Lets a webapp register, check, and remove background scheduled tasks on behalf of the signed-in user. Tasks call a script that lives inside the webapp's own folder — not in user virtual storage.

Prerequisite — the user must have granted cron-job permission to this app first. The recommended flow is:

  1. Check scheduler.hasPermission() in a backend .agi script.
  2. If false, return a signal to the frontend so it can call ao_module_requestSchedulerPermission() to show the permission dialog.
  3. After permission is granted, register the task from the backend.

Scheduler Functions

scheduler.hasPermission()bool

Returns true when the current user is allowed to create scheduled tasks.

requirelib("scheduler");
if (!scheduler.hasPermission()) {
    sendResp("no_permission");
}

scheduler.registered(taskName, appName)bool

Returns true when a task with the given name is already registered for this user+app combination.

requirelib("scheduler");
if (scheduler.registered("MyApp_DailySync", "MyApp")) {
    sendResp("already_registered");
}

scheduler.register(taskName, appName, intervalSecs [, description [, scriptName]])bool

Registers a new background task. Returns true on success.

Parameter Type Description
taskName string Unique task identifier (max 32 chars)
appName string Module folder name (must match ./web/<appName>/)
intervalSecs number Execution interval in seconds
description string Optional human-readable description
scriptName string Script filename inside the app folder (default: "cron.agi")
requirelib("scheduler");
var ok = scheduler.register("MyApp_DailySync", "MyApp", 86400, "Daily maintenance", "cron.agi");
if (!ok) {
    sendResp("register_failed");
}

scheduler.unregister(taskName)bool

Removes a previously registered task. Returns true on success.

requirelib("scheduler");
scheduler.unregister("MyApp_DailySync");

Script Location

The cron script must reside inside the webapp's own web folder, not in user virtual storage:

./web/MyApp/
    init.agi          ← module registration
    index.html
    backend.agi       ← called by the frontend to register/query scheduler
    cron.agi          ← executed by the scheduler at each interval

The scheduler calls cron.agi with the permissions of the user who approved it, so all file-system and database operations are scoped to that user.


sharedspace API

Load:

requirelib("sharedspace");

A shared space is an area where multiple different users can share texts, images, files and collaboratively edited documents together - the collaboration backbone behind chat-style apps, document collaboration and MeetRoom meetings. Every space carries:

  • an access mode: "open" (default - the random space ID acts as the capability, anyone who knows it can read and post), "public" (discoverable via listPublicSpaces, any logged-in user can self-join) or "private" (members only, invited by the owner or a space admin);
  • a member list with roles owner / admin / member;
  • a metadata key-value store (managers only, capped);
  • a persistent flag: persistent spaces (and their items, files and documents) survive server restarts; ephemeral spaces (the default, and what MeetRoom uses) are gone after a restart.

Web clients get the same feature set over /system/sharedspace/* and a realtime WebSocket channel (/system/sharedspace/ws), so items and document patches posted from AGI appear live in connected clients. MeetRoom binds one space to every meeting room (see the meetroom API), so this library is also how scripts read a live meeting's chat and post messages or files into it.

sharedspace.createSpace(name) → object

Create a new space owned by the calling user. Returns { spaceid, name, owner, items, createdat }.

requirelib("sharedspace");
var space = sharedspace.createSpace("Design sync");
sendJSONResp(space);

sharedspace.listMySpaces() → array

List the spaces owned by the calling user (same fields as createSpace).

sharedspace.getSpaceInfo(spaceid) → object

Describe a space by ID. Returns { exists: false } for unknown IDs.

sharedspace.addText(spaceid, text) → string | null

Post a text snippet (clipped to 4000 characters) into the space. Returns the new item ID, or null on failure.

sharedspace.addFile(spaceid, vpath) → string | null

Copy a file from the calling user's storage into the space. Files with a raster-image extension (png / jpg / jpeg / gif / webp / bmp) are stored as image items, everything else as file. Returns the new item ID.

requirelib("sharedspace");
var itemid = sharedspace.addFile(spaceid, "user:/Photo/cat.png");

sharedspace.notifyMembers(spaceid, title, message, priority, usernames) → number

Raise a notification to fellow members of a space through the ArozOS notification system. Membership-scoped: only a member (or space manager) may notify, and only current members can be reached, so this cannot be used to spam arbitrary users (no admin permission required, unlike notification.sendToUser). Members currently connected to the space's realtime channel are skipped, since they already receive the live message. Delivery per recipient follows that user's own notification preferences (desktop, Telegram, email, webhook).

message, priority ("low" / "medium" / "high", default "medium") and usernames are optional. When usernames (an array) is omitted every other member is notified; when given, the recipients are narrowed to that set intersected with the current members. Returns the number of members actually notified, or -1 on error.

requirelib("sharedspace");
//Notify everyone else in the space
sharedspace.notifyMembers(spaceid, "New message", "Alice: are we still on?");
//Notify just two members, at high priority
sharedspace.notifyMembers(spaceid, "Alice in #plan", "@bob @carol ping", "high", ["bob", "carol"]);

sharedspace.listItems(spaceid) → array | null

Chronological list of items: { itemid, type, name, text, size, uploader, origin, time }. type is "text", "image" or "file"; text is only filled for text items.

sharedspace.getText(spaceid, itemid) → string | null

Read the content of a text item.

sharedspace.saveFileTo(spaceid, itemid, destVpath) → bool

Copy an image / file item into the calling user's storage.

sharedspace.removeItem(spaceid, itemid) → bool

Remove an item. Only the item's uploader, a space admin or the space owner may remove it.

sharedspace.deleteSpace(spaceid) → bool

Delete a space and all its items. Space owner or a space admin only.

sharedspace.createSpaceAdvanced(name, options) → object | null

Create a space with options: { access, persistent, metadata }. Returns the space descriptor ({ spaceid, name, owner, access, persistent, items, docs, members, metadata, createdat } - the same shape every listing below uses), or null when e.g. persistence is disabled by the administrator.

requirelib("sharedspace");
var space = sharedspace.createSpaceAdvanced("Team chat", {
    access: "private",
    persistent: true,
    metadata: { purpose: "chat" }
});

sharedspace.listPublicSpaces() → array

The public space directory (descriptors of every "public" space).

sharedspace.listJoinedSpaces() → array

Descriptors of every space the calling user belongs to (owner, admin or member). listMySpaces() remains the owned-only subset.

sharedspace.joinSpace(spaceid) / sharedspace.leaveSpace(spaceid) → bool

Self-join a public (or open) space as a member / leave a space. Private spaces are invite-only, so joinSpace fails on them.

sharedspace.setAccess(spaceid, access) → bool

Change the access mode ("open", "public" or "private"). Managers only.

sharedspace.setMeta(spaceid, key, value) / sharedspace.getMeta(spaceid) → bool / object

Set (empty value deletes) or read the space metadata. Writing requires manager rights; reading requires read access.

sharedspace.addMember(spaceid, username, role) → bool

Invite a user with role "admin" or "member" (default). Managers only.

sharedspace.removeMember(spaceid, username) → bool

Remove a member (managers) or leave (self). The owner cannot be removed.

sharedspace.listMembers(spaceid) → object | null

Member map { username: role, ... }. Members and managers only.

sharedspace.createDoc(spaceid, name) → object | null

Create a collaborative document. Returns the document snapshot { docid, name, creator, revision, content, createdat, updatedat, updatedby }.

sharedspace.listDocs(spaceid) → array | null

Document snapshots without their content.

sharedspace.getDoc(spaceid, docid) → object | null

One document with its full content - also the recovery path after an update conflict.

sharedspace.updateDoc(spaceid, docid, baserev, content) → object | null

Compare-and-swap update: the new content replaces the document body only when baserev matches the document's current revision. Returns { ok: true, revision } on success or { ok: false, conflict: true, revision } when someone else updated the document first - re-fetch with getDoc, merge your change and retry. Debounce saves (~500ms): every accepted revision is one database write, and live WebSocket clients receive each revision as a patch frame.

requirelib("sharedspace");
var doc = sharedspace.getDoc(spaceid, docid);
var result = sharedspace.updateDoc(spaceid, docid, doc.revision, doc.content + "\nAppended by AGI");
if (result !== null && result.conflict) {
    // someone got there first: re-fetch, rebase, retry
}

sharedspace.deleteDoc(spaceid, docid) → bool

Delete a document. Creator or space managers only.


meetroom API

Load:

requirelib("meetroom");

Control interface for the MeetRoom video conferencing WebApp. The library is only injected for users who have access permission to the MeetRoom module (the same gate as the /system/meetroom/* endpoints). Every meeting room owns a shared space where the room's chat and file attachments are mirrored; posting into that space with the sharedspace API delivers the item into the live meeting chat.

meetroom.createRoom(title, password) → object

Create a meeting room hosted by the calling user. Both arguments are optional ("" for an untitled / open room). Returns { roomid, displayid, title, host, protected, participants, createdat, spaceid } where spaceid is the room's shared space.

requirelib("meetroom");
var room = meetroom.createRoom("Weekly standup", "");
sendJSONResp({ invite: room.displayid, space: room.spaceid });

meetroom.getRoomInfo(roomid) → object | null

Describe a room. Returns { exists: false } for unknown IDs. spaceid is included only when the calling user is the room's host.

meetroom.getRoomSpace(roomid, password) → string | null

Return the room's shared space ID after passing the same password check the join endpoint applies. Use this to chat with a meeting you were invited to:

requirelib("meetroom");
requirelib("sharedspace");
var spaceid = meetroom.getRoomSpace("123456789", "roomPassword");
if (spaceid !== null) {
    sharedspace.addText(spaceid, "Reminder: meeting notes are due today");
}

meetroom.listMyRooms() → array

List the live rooms hosted by the calling user (same fields as createRoom).

meetroom.endRoom(roomid) → bool

End the meeting for everyone. Host only.

meetroom.getAttendance(roomid) → array | null

Export the room's attendance log: { username, joinedat, leftat, present } per join (leftat is 0 while the participant is still connected). Only the host or a currently connected participant may read it.

requirelib("meetroom");
var log = meetroom.getAttendance("123456789");
if (log !== null) {
    var report = "";
    for (var i = 0; i < log.length; i++) {
        report += log[i].username + "," + log[i].joinedat + "," + log[i].leftat + "\n";
    }
    if (requirelib("filelib")) {
        filelib.writeFile("user:/Desktop/attendance.csv", report);
    }
}

notification API

Load:

requirelib("notification");

Raise notifications to ArozOS users through the core notification system. The delivery channel (Telegram, desktop, email, custom webhook) is decided by each receiving user's own notification preferences; the script only chooses the priority so users receive it according to their settings. Available only when the host wired a notification sender into the AGI gateway.

Priority is "low", "medium" (default) or "high". Convenience constants notification.PRIORITY_LOW, notification.PRIORITY_MEDIUM and notification.PRIORITY_HIGH are provided.

notification.send(title, message, priority)

Send a notification to the current user. Returns true on success.

requirelib("notification");
notification.send("Backup done", "Your nightly backup finished");
notification.send("Disk failing", "SMART error on /dev/sda", notification.PRIORITY_HIGH);

notification.sendToUser(username, title, message, priority)

Send a notification to another user. Requires admin permission. Returns true on success.

requirelib("notification");
notification.sendToUser("bob", "Hi Bob", "A message for you", "low");

git API

Load:

requirelib("git");

Version control for folders inside the user's virtual file system, backed by go-git — no git binary is required on the host. Available only when the host wired a git manager into the AGI gateway.

Path rules

  • Every path is a virtual path and is permission checked: read-only calls need read permission, mutating calls need write permission.
  • Any path inside a working tree resolves to the repository containing it.
  • The storage pool must be local. Network-backed pools (WebDAV, SMB, S3, …) cannot host a working tree, and every call against one fails with a readable error.

Return convention

Query calls (status, log, branches, remotes, diff, …) return their payload directly, or an object carrying an error string when they fail. Mutating calls (init, clone, commit, push, …) return {success, error, message}. When a remote rejects the credentials the reply also carries authRequired: true, which is the signal to ask the user to sign in and retry.

Credentials

HTTPS username + token pairs are stored per ArozOS user, encrypted with AES-256-GCM, keyed by remote host. A transport call with no explicit credentials automatically uses the stored one for that host. Passing remember: true in the options saves the credential once the operation has actually succeeded. Tokens are never readable back from a script.

git.isRepo(vpath) → bool

if (git.isRepo("user:/Desktop/myproject")) { /* … */ }

git.repoRoot(vpath) → string | false

Virtual path of the working tree root containing vpath.

git.init(vpath) → object

Create an empty repository, creating the folder if needed.

git.clone(url, vpath, options) → object

Clone into vpath, which must be empty or absent. Options: username, token, remember, branch, depth.

var result = git.clone("https://github.com/tobychui/arozos.git", "user:/Desktop/arozos", {
    username: "tobychui",
    token: "ghp_…",
    remember: true,
    depth: 1
});
if (!result.success && result.authRequired) { /* ask the user to sign in */ }

git.status(vpath) → object

The full snapshot used by GitApp: branch, detached, head, upstream, ahead, behind, clean, changes[], remotes[], conflicted.

Each entry of changes carries path, status (added / modified / deleted / renamed / copied / untracked / conflicted), staging, worktree, staged, binary, size and preview (image / pdf / video / audio, or absent when the browser cannot render the file).

var status = git.status("user:/Desktop/myproject");
console.log(status.branch + ": " + status.changes.length + " changed files");

git.log(vpath, limit) → array

Commits reachable from HEAD, newest first (default limit 50). Each commit has hash, shortHash, subject, message, authorName, authorEmail, timestamp, parents and tags (names of any tags pointing at it).

git.branches(vpath) → array

Local and remote-tracking branches: name, fullRef, hash, isRemote, isCurrent, plus remote and short — for origin/feature/login those are origin and feature/login, which is what the branch-management calls below expect. For a local branch short equals name and remote is empty.

git.deleteBranch(vpath, branch, force) → object

Delete a local branch. The checked-out branch is never deleted. A branch holding commits unreachable from HEAD is refused with unmerged: true unless force is set, mirroring git branch -d versus -D.

var result = git.deleteBranch("user:/Desktop/myproject", "old-feature", false);
if (result.unmerged) {
    // confirm with the user, then retry with force
    git.deleteBranch("user:/Desktop/myproject", "old-feature", true);
}

git.renameBranch(vpath, oldName, newName) → object

Rename a local branch. Its upstream configuration moves with it, and HEAD follows when the renamed branch is the checked-out one.

git.deleteRemoteBranch(vpath, remote, branch, options) → object

Delete a branch on the remote — a network push with an empty source refspec. The local remote-tracking ref is pruned too, so the branch stops appearing in git.branches(). The matching local branch, if any, is left alone. Options: username, token, remember.

git.renameRemoteBranch(vpath, remote, oldName, newName, options) → object

Rename a branch on the remote. Git cannot rename a remote ref, so this pushes the new name and then deletes the old one — in that order, so an interrupted rename leaves the branch under both names rather than losing it. Options: username, token, remember.

git.checkout(vpath, branch, create) → object

Switch branches, or create the branch first when create is true. Checking out a remote name such as "origin/feature" creates the matching local branch.

git.remotes(vpath) → array

Configured remotes: name and urls.

git.addRemote(vpath, name, url) / git.removeRemote(vpath, name) → object

Adding an existing remote name replaces its URL.

git.add(vpath, files) / git.addAll(vpath) → object

Stage the given repo-relative paths, or every change. Deleted paths are removed from the index.

git.unstage(vpath, files) → object

Remove paths from the index, leaving the working tree untouched.

git.discard(vpath, files) → object

Throw away working tree changes. Untracked files are deleted, since there is nothing to restore them from.

git.commit(vpath, message, files, options) → object

Stage files and commit them in one step. Options: name, email, all. The author defaults to the calling ArozOS user, then to the repository's own git config. Returns {success, hash, message}.

var result = git.commit("user:/Desktop/myproject", "Fix the parser", ["src/parser.go"], {
    name: "Toby Chui",
    email: "toby@example.com"
});

git.ignore(vpath, patterns) → object

Append rules to the repository's .gitignore, which is created when absent. Rules already present are skipped, so repeating the call is harmless, and the existing content is never rewritten. Returns {success, message} where the message names the rules actually added.

git.ignore("user:/Desktop/myproject", ["/build", "*.log"]);

git.diff(vpath, file) → object

Diff of one path between HEAD and the working tree: additions, deletions, binary, tooLarge, isNew, isDeleted and hunks[]. Each hunk has header, oldStart / oldLines, newStart / newLines and lines[], where every line is {type: "context" | "add" | "del", oldLine, newLine, content}.

git.diffCommit(vpath, hash, file) → object

Same shape, comparing a commit against its first parent.

git.commitFiles(vpath, hash) → array

The paths a commit touched, each with a status and a preview kind.

git.fileBlob(vpath, file, revision) → object

Read a file's content at a revision, returning {success, exists, base64, mime, kind, size}. revision is "HEAD" (the default) or a full 40 character commit hash — branch names and short hashes are rejected.

This is how a committed version of a binary file is obtained: it exists only inside the object database, so unlike the working tree copy it cannot be fetched through the normal media endpoint. exists is false — without an error — when the path was simply not part of that revision, which distinguishes "added in this change" from a read failure. Files above 8 MB are refused.

var blob = git.fileBlob("user:/Desktop/myproject", "img/logo.png", "HEAD");
if (blob.exists) {
    // blob.base64 holds the committed image, blob.mime is "image/png"
}

History actions

Operations on a single commit, used by the GitApp History tab. Each returns {success, error, message}, and the ones that create a commit also return its hash.

  • git.checkoutCommit(vpath, hash) — check the commit out in detached HEAD state. Refuses when the working tree is dirty.
  • git.resetToCommit(vpath, hash, mode) — move the current branch to the commit. mode is "soft", "mixed" (default) or "hard"; a hard reset refuses when there are uncommitted changes.
  • git.createBranchAt(vpath, branch, hash) — create a branch at the commit and check it out.
  • git.createTag(vpath, tag, hash, message) — tag the commit. A non-empty message makes an annotated tag, otherwise a lightweight one.
  • git.revertCommit(vpath, hash, options) — create a new commit undoing the commit's changes.
  • git.cherryPickCommit(vpath, hash, options) — apply the commit's changes onto the current HEAD, preserving the original author.
  • git.amendMessage(vpath, message, options) — rewrite the message of the HEAD commit, keeping its tree, parents and author.

Revert and cherry-pick use a clean-or-refuse strategy: because go-git has no merge engine, a change is applied only when the files it touches still hold the exact content it expects. If any file has diverged the whole operation is refused with a readable message rather than producing an incorrect result. This covers the common cases (reverting the latest commit, or an older commit whose files were untouched since) and safely declines the rest.

var result = git.revertCommit("user:/Desktop/myproject", commitHash, {});
if (!result.success) {
    console.log(result.error); // e.g. "cannot apply cleanly — …"
}

git.fetch(vpath, options) / git.pull(vpath, options) / git.push(vpath, options) → object

Options: remote (default origin), branch (default the current branch), username, token, remember, force, setUpstream.

Only fast-forward merges are supported by the underlying library, so a diverged branch is reported as an error rather than being merged.

var result = git.push("user:/Desktop/myproject", { setUpstream: true });
if (!result.success && result.authRequired) { /* prompt, then retry with token */ }

git.saveCredential(host, username, token) → object

Store a credential for a host. host may be a bare host name or a full remote URL.

git.listCredentials() → array

Stored credentials as {host, username}. Tokens are never returned.

git.hasCredential(host) → bool

git.removeCredential(host) → object

git.remoteHost(url) → string

The host a remote URL maps to, i.e. the key credentials are stored under. Both https://github.com/a/b.git and git@github.com:a/b.git yield github.com.


Examples

Background Scheduler (webapp backend)

A typical webapp has three files that work together to set up a background task.

./web/MyApp/backend.agi — called from the frontend via ao_module_agirun:

requirelib("scheduler");

var APP  = "MyApp";           // matches ./web/MyApp/
var TASK = "MyApp_HourlySync";

var action = getPara("action");

if (action === "status") {
    // Report current scheduler state and persisted stats
    var TABLE = "MyApp/" + USERNAME;
    newDBTableIfNotExists(TABLE);
    var lastRun  = readDBItem(TABLE, "lastRun");
    var runCount = parseInt(readDBItem(TABLE, "runCount") || "0", 10);
    sendJSONResp({
        hasPermission: scheduler.hasPermission(),
        registered:    scheduler.registered(TASK, APP),
        lastRun:       lastRun  || null,
        runCount:      runCount
    });

} else if (action === "register") {
    if (!scheduler.hasPermission()) {
        sendResp("no_permission");
    } else if (scheduler.registered(TASK, APP)) {
        sendResp("already_registered");
    } else {
        var ok = scheduler.register(TASK, APP, 3600, "Hourly sync for MyApp");
        sendResp(ok ? "ok" : "error");
    }

} else if (action === "unregister") {
    scheduler.unregister(TASK);
    sendOK();

} else {
    sendJSONResp({error: "unknown action"});
}

./web/MyApp/cron.agi — executed by the scheduler at each interval:

// Runs with the permissions of the user who approved the task.
// USERNAME, EXECUTION_ID and all standard globals are available.

var TABLE = "MyApp/" + USERNAME;
newDBTableIfNotExists(TABLE);

// Persist a timestamp and run counter for the frontend to display
writeDBItem(TABLE, "lastRun", new Date().toISOString());
var count = parseInt(readDBItem(TABLE, "runCount") || "0", 10);
writeDBItem(TABLE, "runCount", String(count + 1));

// EXECUTION_ID is a UUIDv4 unique to this invocation — appears in scheduler logs
console.log("MyApp tick [" + EXECUTION_ID + "] user=" + USERNAME + " run=" + (count + 1));

sendOK();

./web/MyApp/index.html — frontend snippet that requests permission and polls stats:

<script src="../script/ao_module.js"></script>
<script>
var APP  = "MyApp";
var TASK = "MyApp_HourlySync";

function checkStatus() {
    ao_module_agirun("MyApp/backend.agi", {action: "status"}, function(data) {
        document.getElementById('last-run').textContent =
            data.lastRun ? new Date(data.lastRun).toLocaleString() : "Never";
        document.getElementById('run-count').textContent = data.runCount;
        if (!data.registered && data.hasPermission) {
            document.getElementById('btn-enable').style.display = '';
        }
    });
}

function enableScheduler() {
    ao_module_requestSchedulerPermission({
        appName:     APP,
        appIcon:     APP + "/img/icon.png",
        taskName:    TASK,
        scriptName:  "cron.agi",   // filename inside ./web/MyApp/
        interval:    3600,         // seconds
        description: "Hourly sync for MyApp."
    }, function(result) {
        if (result && result.allowed) checkStatus();
    });
}

checkStatus();
setInterval(checkStatus, 30000);
</script>

This documentation covers all available AGI APIs with practical examples. For more advanced usage, refer to the existing module implementations in the system.

Notes and Caveats

  • requirelib("audio") is registered in code but currently has no callable functions.
  • filelib currently does not expose writeBinaryFile / readBinaryFile on the public filelib object.
  • Most APIs return false or null on failure; many also raise AGI runtime errors.
  • For admin-only APIs (userExists, createUser, removeUser), check userIsAdmin() first.