|
|
1 неделя назад | |
|---|---|---|
| .. | ||
| static | 1 месяц назад | |
| README.md | 1 неделя назад | |
| agi.7z.go | 1 неделя назад | |
| agi.aichat_backend_test.go | 1 неделя назад | |
| agi.aimodel.go | 1 неделя назад | |
| agi.aimodel_anthropic_test.go | 1 неделя назад | |
| agi.aimodel_test.go | 1 неделя назад | |
| agi.appdata.go | 2 недель назад | |
| agi.audio.go | 2 недель назад | |
| agi.ffmpeg.go | 2 недель назад | |
| agi.file.go | 2 недель назад | |
| agi.go | 1 неделя назад | |
| agi.http.go | 2 недель назад | |
| agi.image.go | 1 неделя назад | |
| agi.image_test.go | 1 неделя назад | |
| agi.iot.go | 2 недель назад | |
| agi.scheduler.go | 2 недель назад | |
| agi.share.go | 2 недель назад | |
| agi.sqlite.go | 1 неделя назад | |
| agi.sqlite_stub.go | 1 неделя назад | |
| agi.sysinfo.go | 2 недель назад | |
| agi.system.go | 2 недель назад | |
| agi.user.go | 2 недель назад | |
| agi.vm_registry.go | 2 недель назад | |
| agi.websocket.go | 2 недель назад | |
| agi.zip.go | 1 неделя назад | |
| agi_scheduler_test.go | 3 недель назад | |
| agi_sqlite_test.go | 1 неделя назад | |
| error.go | 2 недель назад | |
| externalReqHandler.go | 2 недель назад | |
| handler.go | 2 лет назад | |
| moduleManager.go | 1 неделя назад | |
| serverlessReqHandler.go | 2 лет назад | |
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 afunctionsarray of{ name, sig, desc, ret, example }entries.
3.0 (AgiVersion in agi.go)// 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);
}
BUILD_VERSIONINTERNAL_VERSIONLOADED_MODULESLOADED_STORAGES__FILE__HTTP_RESPHTTP_HEADERUSERNAMEUSERICONUSERQUOTA_TOTALUSERQUOTA_USEDUSER_VROOTSUSER_MODULESexecd child)BUILD_VERSION: Current system build versionINTERNAL_VERSION: Internal version numberLOADED_MODULES: Array of loaded system modulesLOADED_STORAGES: Array of available storage pools__FILE__: Current script file pathHTTP_RESP: Response content (automatically set)HTTP_HEADER: Response content type (automatically set)USERNAME: Current user's usernameUSERICON: Current user's icon pathUSERQUOTA_TOTAL: User's total storage quotaUSERQUOTA_USED: User's used storage quotaUSER_VROOTS: User's accessible virtual root pathsUSER_MODULES: User's accessible modulesEXECUTION_ID: UUIDv4 that uniquely identifies this script invocation — useful for correlating log lines across concurrent executionsPARENT_DETACHED (true)PARENT_PAYLOAD (string payload)user:/, tmp:/, extuuid:/....CanRead / CanWrite).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] });
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");
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");
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" }));
requirepkg(...) -> deprecated in AGI v3execpkg(...) -> deprecated in AGI v3decodeVirtualPath(...) -> deprecateddecodeAbsoluteVirtualPath(...) -> deprecatedencodeRealPath(...) -> deprecatedpathCanWrite(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.
Use requirelib(libName).
requirelib("filelib");
Registered library IDs:
filelibimagelibhttpshareiotappdatasysinfoziplibaimodel (OpenAI / Anthropic LLM chat: text & file based, with pricing & quota)ffmpeg (only when ffmpeg exists on host)Special case:
requirelib("websocket") is injected only in HTTP request context.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");
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"));
Load:
requirelib("http");
http.get(url)var body = http.get("https://example.com");
http.post(url, jsonString)var body = http.post("https://example.com/api", JSON.stringify({a:1}));
http.head(url, headerKey)headerKey: returns JSON string of all headers.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);
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);
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");
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();
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();
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");
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)ffmpeg.imageConvert("user:/in.png", "user:/out.jpg", 0.5, 80);
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");
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 upgrade —websocket.upgrade()replaces the globaldelay()with a message-pumping version. While the script sleeps insidedelay(), any queued inbound frames are dispatched towebsocket.onMessage(if set).delay()is therefore the natural yield point in event-driven loops. WhenonMessageisnullthe buffer is left untouched so thatavailable()andread()can still see the frames.
websocket.upgrade(timeoutSec) → boolUpgrades 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) → boolSends 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 | falseReads 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() → numberReturns 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() → boolReturns true when the WebSocket connection is no longer active.
while (!websocket.isClosed()) {
websocket.send("tick");
delay(1000);
}
websocket.onMessageAssign 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();
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();
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
}
}
onMessage callback with delay() pumpEvent-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)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:
- Check
scheduler.hasPermission()in a backend.agiscript.- If false, return a signal to the frontend so it can call
ao_module_requestSchedulerPermission()to show the permission dialog.- After permission is granted, register the task from the backend.
scheduler.hasPermission() → boolReturns true when the current user is allowed to create scheduled tasks.
requirelib("scheduler");
if (!scheduler.hasPermission()) {
sendResp("no_permission");
}
scheduler.registered(taskName, appName) → boolReturns 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]]) → boolRegisters 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) → boolRemoves a previously registered task. Returns true on success.
requirelib("scheduler");
scheduler.unregister("MyApp_DailySync");
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.
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.
requirelib("audio") is registered in code but currently has no callable functions.filelib currently does not expose writeBinaryFile / readBinaryFile on the public filelib object.false or null on failure; many also raise AGI runtime errors.userExists, createUser, removeUser), check userIsAdmin() first.