Directive & Remote Computing

DIRECTIVE sends commands to a registered remote machine. The oql-client agent is installed on any machine — it connects outbound to ql.ocalt.com, receives commands, and executes them natively. No inbound ports. No firewall rules required. The agent does not run an OcaltQL runtime — it executes shell commands, file transfers, and system operations directly. All OcaltQL logic stays on the server side.

Implementation status. Everything on this page is implemented and working.

Inline Operations

Every DIRECTIVE operation is a single inline statement — there is no OPEN ... CLOSE block form. The agent executes shell commands, file transfers, and system operations directly; all OcaltQL logic (conditionals, loops, variable handling) stays on the server. EXEC can run any shell command on the target machine, which means any operation the machine supports is reachable through it.

Machine Slots

Every machine must claim a slot before it can be addressed. DIRECTIVE REGISTER claims one for a machine id of your choosing — that id is what you use in every later DIRECTIVE call. Calling DIRECTIVE against an id that was never registered is a fatal error.

Every account holds 4,096 machine slots — the same on every plan. A slot is taken when a machine registers and released when it is removed.

A released slot is held for 24 hours. Within that window the machine id cannot be re-registered, which keeps an id stable for anything already addressing it. This is a mechanism, not a plan limit — it applies identically on every plan.

A claimed slot is held for 24 hours. Within that window it cannot be released or reassigned — this keeps a machine id stable for anything already addressing it. After 24 hours, DIRECTIVE UNREGISTER frees the slot for another machine.

Registering never displaces a machine you already have. If every slot is in use, DIRECTIVE REGISTER fails with a fatal error telling you either to release a slot, or how many hours remain until the oldest one can be released. Re-registering an id you already hold is harmless and does not consume a second slot.
Claim a Slot, Then Use the Machine
DIRECTIVE REGISTER "home-nas" SET ?reg
AFTER EMIT ?reg("slots")
AFTER EMIT ?reg("used")
AFTER DIRECTIVE "home-nas" EXEC "uptime" SET ?r
AFTER EMIT ?r("stdout")
Release a Slot After 24 Hours
DIRECTIVE UNREGISTER "old-laptop" SET ?rel
AFTER EMIT ?rel
AFTER DIRECTIVE REGISTER "new-laptop" SET ?reg
AFTER EMIT ?reg
Fatal Errors
DIRECTIVE "never-registered" EXEC "echo hi"
(* Fatal: unregistered machine id 'never-registered' — claim a slot with DIRECTIVE REGISTER first *)

DIRECTIVE REGISTER "one-too-many"
(* Fatal: 'one-too-many' was registered less than 24 hours ago and
   cannot be re-registered yet — it unlocks in 19 hour(s) *)

Agent Management

List and Check Agents
DIRECTIVE LIST SET ?agents
AFTER EMIT ?agents("agents")
AFTER EMIT ?agents("used")
AFTER EMIT ?agents("slots")
AFTER DIRECTIVE STATUS "my-home-server" SET ?status
AFTER EMIT ?status("online")

POLL — How Often an Agent Checks In

An agent polls for work on an interval. POLL sets it, per machine, and takes effect on that machine’s next check-in.

Setting the Interval
DIRECTIVE "my-server" POLL 1 SECONDS
AFTER DIRECTIVE "home-nas" POLL 5 MINUTES
AFTER DIRECTIVE "backup-box" POLL 60 MINUTES
The default is 1 second. The minimum is 1 SECOND and the maximum is 60 MINUTES — anything outside that range is an error. A shorter interval means a command reaches the machine sooner; a longer one is kinder to a metered or battery-powered connection.

EXEC and KILL — Remote Shell

Shell commands run directly on the target machine. BACKGROUND makes the call non-blocking, returning a process handle immediately instead of waiting for completion.

Blocking Command
DIRECTIVE "my-server" EXEC "apt-get update && apt-get install -y ffmpeg" SET ?r
AFTER EMIT ?r("stdout")
Background Process and Kill
DIRECTIVE "my-server" EXEC "python3 /home/worker.py" BACKGROUND SET ?pid
AFTER SLEEP 5 SECONDS
AFTER DIRECTIVE "my-server" KILL ?pid

DOWNLOAD and UPLOAD

Transfers stream directly between the machine and your namespace, so file size is bounded by your storage quota rather than by memory. DOWNLOAD pulls a file from the machine into the namespace; UPLOAD pushes one the other way. Namespace paths are absolute — /root/... or /mounted/....

Transfers run synchronously by default — the next statement waits until the file has finished moving. To keep working while a transfer runs, make it a promise with SET PROMISE: the transfer starts in the background, the variable reads WAITING until it is done, and WAIT FOR collects the result.

Synchronous Download
DIRECTIVE "home-nas" DOWNLOAD "/srv/reports/latest.csv" TO "/root/latest.csv" SET ?downl
AFTER EMIT ?downl("state")
(* First path is on the machine, second is in your namespace.
   Synchronous: this line does not run until the transfer completes. *)
Upload
DIRECTIVE "my-server" UPLOAD "/root/config.json" TO "/etc/app/config.json" SET ?upl
Non-Blocking Transfer with SET PROMISE
DIRECTIVE "my-server" DOWNLOAD "/var/big.bin" TO "/root/big.bin" SET PROMISE ?job
AFTER EMIT "transfer started, script keeps running"
AFTER IF ?job IS WAITING OPEN EMIT "still transferring" CLOSE
AFTER WAIT FOR ?job SET ?done
AFTER EMIT ?done("state")
(* Fires without blocking; ?job is WAITING until it lands, then WAIT FOR collects it *)

PEERS and NETWORK — Machine-to-Machine Transfer

Not yet implemented. Two machines behind different routers cannot simply open a socket to each other — that is the same NAT traversal problem TURN solves, and these are being built together rather than twice. Until then, a transfer between two machines goes through your namespace: DOWNLOAD from one, UPLOAD to the other. Slower for a large file on the same LAN, and correct everywhere.

DIRECTIVE PEERS groups several registered machines into one network value. DIRECTIVE NETWORK ... TRANSFER then moves a file directly between two of them — the bytes travel machine to machine, not up through the server and back down. Each endpoint is written as a machine id, then ://, then a path on that machine.

Build a Peer Network
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER EMIT ?network(0)("id")
AFTER COUNT ?network SET ?n
AFTER EMIT ?n & " peers"
Transfer Between Two Peers, Then Poll It
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER DIRECTIVE NETWORK ?network TRANSFER FROM ?network(0)('id') & "://c:/downloads/file.zip" TO ?network(1)('id') & "://var/www/folder/destfile.zip" SET ?statusanchor
AFTER DIRECTIVE STATUS ?statusanchor SET ?progress
AFTER IF ?progress IS IDENTICAL TO "buffering"
OPEN
  SLEEP 5 SECONDS
CLOSE
OR IF ?progress IS IDENTICAL TO "done"
OPEN
  EMIT "peer transfer done"
CLOSE
OR
OPEN
  EMIT "still in progress check back later"
CLOSE
Share a Peer’s Port Across the Network
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER DIRECTIVE NETWORK ?network SHARE ?network(0)('id') PORT 80 AS 8080 SET ?status
(* Any machine in the network can now reach node 0's port 80
   at its own localhost:8080 *)
SHARE is peer-to-peer, not a public tunnel. PORT names the port on the machine being shared; AS is the local port every other peer binds. Nothing is exposed to the internet — only members of the network reach it, and only on their own loopback. TUNNEL is the verb for a public URL.
The transfer is non-blocking. TRANSFER returns a status anchor immediately rather than waiting for the file to land. DIRECTIVE STATUS ?statusanchor reads the current state of that transfer — distinct from DIRECTIVE STATUS "id", which reports whether a machine is online. The verb tells them apart by what it is given: a status anchor, or a machine id.

SCREENSHOT and WAKE

Capture the Remote Screen
DIRECTIVE "my-server" SCREENSHOT SET ?shot
AFTER EMIT ?shot
(* ?shot is the path to a real PNG in your namespace *)

The capture is written into your namespace as an ordinary PNG and the verb hands back its path. Nothing is base64, and nothing needs decoding: it is a file, so IMAGE LOAD, GRAPHIC, FILE SHARE and everything else that takes a file work on it directly.

A Screenshot You Can Open in a Browser
DIRECTIVE "laptop" SCREENSHOT SET ?shot
AFTER FILE SHARE ?shot SET ?link
AFTER EMIT ?link("url")
AFTER EMIT `<img src="` & ?link("url") & `">`
(* The URL points at the PNG itself — no wrapper page needed *)
Wake-on-LAN
DIRECTIVE "office-pc" WAKE "AA:BB:CC:DD:EE:FF"
(* An online agent sends the magic packet on its LAN to wake another machine *)
WAKE has a real network constraint. Wake-on-LAN packets are broadcast on the local network segment and do not route across the public internet. WAKE only succeeds if another already-online oql-client agent shares that machine's local network and can relay the magic packet locally.

TUNNEL — Hosting Without a Static IP

Tunnel traffic travels over the connection the agent already holds open to Ocalt — there is no second program to install and no port to open on the machine or the router. Requests arrive at the tunnel URL, are passed down that connection, answered by whatever is listening on the machine's local port, and streamed back.

A tunnel exposes a port on the target machine to the public internet through the agent's own outbound connection — no port forwarding, no static IP, works behind NAT and on mobile connections. Opening one returns a generated URL of the form https://<token>.tunnel.ocalt.com. The tunnel runs independently of the script that created it; closing it deletes the token, and the URL stops working immediately even if the machine is still online.

Exposes a port on the remote machine to the public internet through the agent's already-open outbound connection — no port forwarding, no static IP, works over mobile/cellular connections. The tunnel runs indefinitely once created, independent of the script that started it, and is closed by a separate, later call.

Start and End a Tunnel
DIRECTIVE "home-nas" TUNNEL PORT 3000 SET ?tunnel
AFTER EMIT ?tunnel("url")
(* Returns https://<token>.tunnel.ocalt.com — public traffic reaches localhost:3000 on the agent machine *)
Ending a Tunnel
DIRECTIVE TUNNEL END ?tunnel
Each tunnel gets its own subdomain. The token is the hostname, not a path, so a site behind the tunnel keeps working when its pages reference assets from the site root — /style.css and /img/logo.png resolve inside that tunnel rather than colliding at the top of a shared domain. Nothing is stored in the browser to make this work.
Auto-expiry. If the agent's connection drops and does not reconnect within 5 minutes, the tunnel is automatically torn down and its URL stops resolving.

Tunnel URLs are generated per tunnel and are not tied to your subdomains. Site Mode hosting is unaffected by tunnels: a subdomain always serves its own files, whether or not any tunnel is open.

REDIRECT on the Remote Agent

DIRECTIVE "id" REDIRECT "url" opens a URL on the remote agent machine's own local client — useful when the remote machine itself needs to open something locally, such as joining a video call from its own side. This is distinct from the normal REDIRECT verb, which redirects the visitor who triggered the script.

Two Different Redirect Targets in One Script
NEW VIDEO BRIDGE 2 PEERS SET ?room
AFTER DIRECTIVE "home-server" REDIRECT ?room("peers")(0)("connect_url")
(* Opens the URL on the remote agent's own local client *)
AFTER REDIRECT ?room("peers")(1)("connect_url")
(* Redirects the actual visitor who ran this script *)

File Access — Read & Write

Read and write files directly on the target machine, resolved under the client's configured root folder. Paths are confined to that root.

Write then Read a File
DIRECTIVE "my-server" WRITE "config/app.json" CONTENT "{"mode":"live"}" SET ?w
AFTER DIRECTIVE "my-server" READ "config/app.json" SET ?cfg
AFTER EMIT ?cfg("data")
(* WRITE creates parent folders as needed; READ returns file contents in data *)
Reading and Writing on the Remote Machine
DIRECTIVE "my-server" READ "C:/app/config.json" SET ?cfg
AFTER EMIT ?cfg
Bridging Remote Logs into Local Storage
DIRECTIVE "my-server" READ "/var/log/app.log" SET ?log
AFTER FILE WRITE "/mounted/logs/app.log" CONTENT ?log

Browsing the Machine's Filesystem

LIST FROM returns the contents of a folder on the target machine; LIST DRIVES returns the machine's drives, which is how you find out what paths exist before listing one. Both are read-only and need no desktop session.

List Drives, Then List a Folder
DIRECTIVE "my-server" LIST DRIVES SET ?arrayofdrives
AFTER EMIT ?arrayofdrives(0)
AFTER DIRECTIVE "my-server" LIST FROM "C:/" SET ?cdrivefolderitemarray
AFTER FOREACH ?cdrivefolderitemarray SET ?item
OPEN
  EMIT ?item("name")
CLOSE
Each entry from LIST FROM carries name, is_dir, size and modified — the same shape FILE LIST returns for your own namespace, so the two can be walked by identical code.

SERVE — Expose a File Over the Tunnel

SERVE exposes a single file on the machine, without copying it anywhere. It returns a public url ready to use, and also the local port it bound, so the file can be paired with TUNNEL by hand when a script needs the port for something else. Nothing is installed and nothing is uploaded; the file is read from disk as it is requested.

Serve a File and Get Its URL
DIRECTIVE "laptop" SERVE "C:/media/clip.mp4" SET ?src
AFTER EMIT ?src("url")
(* Returns https://serve.ocalt.com/<token>.mp4 — the served file’s extension is part of the URL, so a browser or player picks the right handler. The file streams from the machine on demand *)
A token path, not a subdomain. A served file sits alone at the root of its URL — there is nothing else to navigate to — so SERVE uses serve.ocalt.com/<token>.<ext> — the token carries the served file’s extension so the URL ends in .mp4, .pdf and the like — while a TUNNEL, which fronts a whole site, gets its own subdomain instead.
Serve a File and Tunnel It Yourself
DIRECTIVE "laptop" SERVE "C:/media/clip.mp4" SET ?src
AFTER DIRECTIVE "laptop" TUNNEL PORT ?src("port") SET ?tunnel
AFTER EMIT ?tunnel("url")
(* The file is now readable at that URL, streamed from the machine on demand *)
Ranged requests are supported, so a video served this way can be seeked and played rather than only downloaded whole. The file is streamed in chunks — nothing is held in memory on either side, so size is not a constraint.

SCREEN — Display Geometry

SCREEN returns one entry per display, so a script can work out where it is pointing before it moves anything. Each entry carries width, height, x, y and primary.

Read the Displays, Then Click the Centre
DIRECTIVE "laptop" SCREEN SET ?screen
AFTER EMIT ?screen(0)("width")
AFTER EMIT ?screen(0)("height")
AFTER CALCULATE ?screen(0)("width") / 2 SET ?cx
AFTER CALCULATE ?screen(0)("height") / 2 SET ?cy
AFTER DIRECTIVE "laptop" POINTER X ?cx Y ?cy
AFTER DIRECTIVE "laptop" MOUSE "[Click]"
(* ?screen(1) is null when there is only one display *)

VIEW — The Screen, Live

SCREENSHOT is a moment; VIEW is a stream. It opens a live feed of the machine’s screen and returns a URL that plays it — no polling, no refresh loop, no series of stills pretending to be video.

Being rebuilt. VIEW previously wrote a single frame to a fixed path, overwriting it each call — which is what SCREENSHOT now does properly, with a file per capture. The live feed rides on the same tunnel and transcode machinery used elsewhere; until it lands, VIEW returns an error pointing you at SCREENSHOT.
Watching a Machine
DIRECTIVE "laptop" VIEW SET ?feed
AFTER EMIT ?feed("url")
(* A tokened URL. Open it in a player or an iframe. *)

AFTER EMIT `<iframe src="` & ?feed("url") & `" allow="autoplay"></iframe>`

Choosing a display

A machine with two monitors has two screens worth seeing, and capturing only the first would quietly hide half of it. ON DISPLAY says which — on SCREENSHOT and on VIEW alike.

One Display, Another, or Every One
DIRECTIVE "laptop" SCREENSHOT SET ?shot
(* the primary display *)

AFTER DIRECTIVE "laptop" SCREENSHOT ON DISPLAY 2 SET ?second
(* the second monitor *)

AFTER DIRECTIVE "laptop" SCREENSHOT ON DISPLAY "all" SET ?shots
AFTER COUNT ?shots SET ?n
AFTER EMIT ?n & " displays captured"
(* ?shots(0) ?shots(1) ... one path per display *)

Displays are numbered from 1, in the order SCREEN reports them, so a script can ask what is there before deciding what to capture.

Every Display, Each Its Own Feed
DIRECTIVE "workstation" VIEW ON DISPLAY "all" SET ?feeds
AFTER FOREACH ?feeds SET ?f
OPEN
  EMIT `<iframe src="` & ?f("url") & `"></iframe>`
CLOSE
(* Three monitors become three players, side by side *)
A feed is one video pipeline, so each display is its own stream rather than several screens squeezed into one picture.

How long a feed lasts

A feed lives while it is being watched. Five minutes after the last time anything pulls from it, it closes and its URL stops working.

Idle is the right thing to measure, not age. A fixed expiry would cut off someone in the middle of watching, and would keep an abandoned tab transcoding long after everyone walked away. This way a feed you are using never dies under you, and a feed nobody is looking at stops costing anything.

Checking Whether a Feed Is Still Alive
DIRECTIVE "laptop" VIEW SET ?feed
AFTER EMIT ?feed("url")
AFTER EMIT ?feed("expires_after")
(* 300 — seconds of inactivity before it closes *)

(* Later, in another script *)
AFTER DIRECTIVE VIEW END ?feed
(* Or just stop watching: it closes on its own. *)
A link to someone’s live screen is a more serious thing to leak than a link to one old frame. It stops working on its own rather than lasting until somebody remembers to revoke it — and if you want it gone now, DIRECTIVE VIEW END closes it immediately.

Look, decide, act

Driving a Machine by Hand
(* Look. *)
DIRECTIVE "my-server" SCREENSHOT SET ?before
AFTER FILE SHARE ?before SET ?link
AFTER EMIT ?link("url")

(* Act, then look again. *)
AFTER DIRECTIVE "my-server" POINTER X 400 Y 300
AFTER DIRECTIVE "my-server" MOUSE "[Click]"
AFTER DIRECTIVE "my-server" SCREENSHOT SET ?after
AFTER FILE SHARE ?after SET ?link2
AFTER EMIT ?link2("url")

(* Two files, two moments — you can see exactly what the click did. *)
These need a desktop session. MOUSE, KEYBOARD, POINTER, SCREENSHOT, VIEW and SCREEN all act on a real display — injecting input into it or reading pixels from it. On a machine with no graphical session, such as a headless server, they return an error about not reaching a display. That is not something a script can work around: the machine needs a logged-in desktop for them to have anything to act on.

Input Control — Mouse, Keyboard & Pointer

Beyond shell access, DIRECTIVE can drive the target machine's actual mouse and keyboard through the OcaltQL Client's input service. The client must be running on the target with input access — combined with SCREENSHOT, this makes a machine fully navigable from a script. Keys and buttons are written as [Name] tokens joined by +: "[Control]+[c]", "[MouseDown]+[Down]". The full grammar is below.

Key and Button Syntax

Keys and buttons are written as [Name] tokens. Combine them with + between brackets — [Control]+[c], [Control]+[Shift]+[Escape]. A quoted string inside the brackets is a literal character, which is how you send the bracket keys themselves: ["]"] is the ] key, ["["] is [. Single characters need no quotes: [a], [5], [/].

Named keys follow the platform key set: [Enter], [Escape], [Tab], [Space], [Backspace], [Delete], [Insert], [Home], [End], [PageUp], [PageDown], [Up] [Down] [Left] [Right] (or [ArrowUp] etc.), [F1][F20], [CapsLock], [NumLock], [ScrollLock], [PrintScreen], [Pause], [ContextMenu], media keys such as [VolumeUp], [VolumeDown], [VolumeMute], [MediaPlayPause], [MediaNextTrack], and numpad keys [Numpad0][Numpad9].

Modifiers: [Control] (or [Ctrl]), [Shift], [Alt], [Meta] (or [Win], [Cmd]). In a combination the last token is the key pressed; everything before it is held down and released after.

Mouse tokens: [Click], [RightClick], [MouseDown], [MouseUp], [Up] [Down] [Left] [Right] (relative movement), [ScrollUp], [ScrollDown]. Combine them the same way: [MouseDown]+[Down] drags downward.

Coordinates are absolute across the whole virtual desktop, not per-monitor. Your primary display's top-left corner is 0,0; a monitor to its right occupies x values beyond the primary's width, and one to its left uses negative x. POINTER COORDINATES reads back in the same space, so you can read a position, work out which display it falls on, and move relative to it.
Move and Read the Pointer
DIRECTIVE "my-server" POINTER X 400 Y 300
AFTER DIRECTIVE "my-server" POINTER COORDINATES SET ?coord
AFTER EMIT ?coord("x")
AFTER EMIT ?coord("y")
(* POINTER COORDINATES blocks for the agent's reply — ?coord("x"), ?coord("y") *)
Mouse Actions
DIRECTIVE "my-server" MOUSE "[MouseDown]+[Down]"
AFTER DIRECTIVE "my-server" MOUSE "[ScrollDown]"
(* Tokens: [Click] [RightClick] [MouseDown] [MouseUp] [Up] [Down] [Left] [Right] [ScrollUp] [ScrollDown] *)
Keyboard Input
DIRECTIVE "my-server" KEYBOARD "[Control]+[c]"
AFTER DIRECTIVE "my-server" KEYBOARD "[Enter]" SET ?ok
AFTER EMIT ?ok
(* Single keys or + combinations. Optional SET returns true/false for success *)
Screenshot-Driven Navigation
DIRECTIVE "my-server" SCREENSHOT SET ?img
AFTER DIRECTIVE "my-server" POINTER X 250 Y 480
AFTER DIRECTIVE "my-server" MOUSE "[Click]"
AFTER DIRECTIVE "my-server" KEYBOARD "[Enter]"
(* Capture, locate a target, move, click, type — the remote navigation loop *)

When Things Fail

Two conditions stop a script outright, because the script named something that cannot work: addressing a machine id that holds no slot, and calling DIRECTIVE REGISTER when every slot is in use. Both raise a fatal error naming the id and, for slot saturation, how long until a slot can be released.

Everything else is reported rather than fatal. A blocking command against an agent that is offline or stops responding waits until the command's ceiling of 300 seconds and then returns a timeout — the row stays on the hub, so if that agent reconnects later it still receives the work. A command the agent could not carry out comes back with ok false and an error describing why, which you can branch on. Use DIRECTIVE STATUS "id" before a long run if you would rather check that a machine is online first.

A tunnel is torn down if its agent stops reconnecting, and closing one revokes it centrally: the URL stops working immediately, whatever state the machine is in.

Rules

Rule Detail
No nestingCannot call DIRECTIVE inside another DIRECTIVE statement
Machine IDMust be a registered slot; the agent must be online except for WAKE, which targets an offline machine through an online relay
FilesystemDIRECTIVE READ/WRITE resolve paths on the remote machine's filesystem
CredentialsThe agent uses the same Ocalt credentials as the namespace that registered it
Machine slots4,096 on every plan — a released slot unlocks after 24 hours

The Machine's Location

Every registered machine reports approximate coordinates, resolved from its public address when the agent starts. No permission prompt, no GPS — it is the same city-level accuracy a web server infers from an IP.

Where a Machine Is
DIRECTIVE STATUS "home-nas" SET ?s
AFTER EMIT ?s("lat") & ", " & ?s("lon")

AFTER DIRECTIVE LIST SET ?machines
AFTER FOREACH ?machines SET ?m
OPEN
  EMIT ?m("id") & " — " & ?m("lat") & "," & ?m("lon")
CLOSE
Coordinates come from the machine’s public address, so a machine behind a VPN reports the VPN’s location, and one with no internet route reports nothing. For a precise fix from a person’s device, use USER DEVICE in a script they load — that asks the browser, and the visitor consents.

A Local Server on the Machine

The agent can serve the machine’s root folder as a website on a port of its own. Static files come straight off local disk; an .oql file executes on Ocalt with the visitor’s request forwarded, exactly as Site Mode does. The same script behaves identically on a subdomain and on the machine.

Set the port in the agent’s configuration page at localhost:12345/config, alongside the root folder. Any free port works — including 80 — except 12345, which the agent’s own interface uses. Blank or 0 turns it off.
The local server listens on the machine’s network, not the internet. To reach it from outside, put a TUNNEL in front of it: DIRECTIVE "home-nas" TUNNEL PORT 80 SET ?t.

Using it as a development server

This is how you build a site without deploying anything. Point the agent’s root at the folder you are working in, give it a port, and open it in a browser — you are editing files on your own machine while real OcaltQL runs against your real namespace.

The Whole Setup
(* 1. Open the agent's own manager on the machine *)
    http://localhost:12345/config

(* 2. Set two fields:
       Root        /home/you/projects/myshop
       Local port  8080                       *)

(* 3. Write a script in that folder *)
    /home/you/projects/myshop/index.oql

(* 4. Open it *)
    http://localhost:8080/

No build step, no upload, no restart. The agent reads the file off disk on every request, so a refresh always shows what you last saved.

How a URL becomes a file

You open It serves
/index.oql in the root folder
/ordersorders.oql — an extensionless path falls back to .oql
/shop/shop/index.oql — a folder serves its index
/style.cssThe file itself, straight off local disk
/video.mp4The file, with byte ranges — so seeking in a player works

That is the same routing Site Mode uses on a subdomain, which is the point: a script that works here works there unchanged, because it is the same script running in the same place. Only the thing that fetched it differs.

What reaches your script

The agent forwards the visitor’s method, query string, body and cookies along with the script, so !GET, !POST, !REQUEST, !COOKIE and START SESSION behave exactly as they will in production. A form you test on localhost posts to the same code that will receive it live.

A Form You Can Test on localhost
(* index.oql *)
IF !REQUEST("method") IS IDENTICAL TO "POST"
OPEN
  EMIT "You sent: " & !POST("message")
CLOSE
OR
OPEN
  EMIT `<form method="post">
          <input name="message">
          <button>Send</button>
        </form>`
CLOSE
The script runs on Ocalt, not on your machine. Only the file is local. Your databases, your namespace files, your machines and your quota are all exactly what they will be in production — there is no second runtime to drift out of step, and nothing to install but the agent.
Anything outside the root folder is refused, and a path containing .. is rejected before it is looked at. Port 12345 cannot be used — that is the agent’s own manager.

Agent Setup

Install the oql-client application on the machine you want to reach. On first run it opens its own local manager at http://localhost:12345, where you enter your Ocalt credentials, a machine alias (the id you target in DIRECTIVE), and a local root folder. The agent stores that config and connects outbound on its own — no inbound port forwarding, and nothing to open on your router.

The manager also shows a live activity log of every command the machine has run, and the agent can be set to start automatically at logon. Once configured, the machine is addressable from any OcaltQL script using its alias.

Full Verb Reference

Verb Description
DIRECTIVE "id" EXEC "cmd" SET ?rRun a shell command, blocking
DIRECTIVE "id" EXEC "cmd" BACKGROUND SET ?pidRun a shell command, non-blocking
DIRECTIVE "id" KILL ?pidTerminate a background process by its operating-system pid — the whole process group is signalled, so child processes die with it
DIRECTIVE "id" DOWNLOAD "remote" TO "local" SET ?hTransfer a file from the remote machine
DIRECTIVE "id" UPLOAD "local" TO "remote" SET ?hTransfer a file to the remote machine
<transfer> SET PROMISE ?jobRun a transfer in the background — ?job is WAITING, collect with WAIT FOR
DIRECTIVE "id" SCREENSHOT SET ?imgCapture the remote machine's screen
DIRECTIVE "id" WAKE "MAC"An online agent broadcasts a Wake-on-LAN packet on its LAN for the given MAC
DIRECTIVE "id" TUNNEL PORT n SET ?tunnelExpose a local port publicly, no port forwarding needed
DIRECTIVE TUNNEL END ?tunnelClose an active tunnel
DIRECTIVE REGISTER "id" SET ?regClaim a machine slot — fatal if every slot is in use
DIRECTIVE UNREGISTER "id" SET ?relRelease a slot, permitted once it is 24 hours old
DIRECTIVE "id" SERVE "path" SET ?srcServe one file straight off the machine — returns a public url plus the local port
DIRECTIVE "id" SCREEN SET ?sDisplay geometry per monitor — ?s(0)("width"), ("height"), ("x"), ("y"), ("primary")
DIRECTIVE "id" SCREENSHOT [ON DISPLAY n|"all"] SET ?pathCapture a display to a timestamped PNG in your namespace
DIRECTIVE "id" VIEW [ON DISPLAY n|"all"] SET ?feedOpen a live feed of a display, returning its URL
DIRECTIVE "id" REDIRECT "url"Open a URL in that machine's own default browser
DIRECTIVE "id" POLL n SECONDS|MINUTESCheck-in interval for one machine — 1 SECOND to 60 MINUTES, default 1 SECOND
DIRECTIVE PEERS "id" AND "id" SET ?networkGroup registered machines into one peer network value
DIRECTIVE NETWORK ?network TRANSFER FROM "id://path" TO "id://path" SET ?anchorMove a file directly between two peers — non-blocking, returns a status anchor
DIRECTIVE NETWORK ?network SHARE "id" PORT n AS n SET ?statusExpose one peer’s port on every other peer’s localhost
DIRECTIVE STATUS ?anchor SET ?progressRead a peer transfer's progress — buffering, done, or in progress
DIRECTIVE "id" LIST FROM "C:/" SET ?itemsList a folder on the machine — name, is_dir, size, modified
DIRECTIVE "id" LIST DRIVES SET ?drivesList the machine's drives
DIRECTIVE LIST SET ?aRegistered machines plus slot accounting — ?a("agents"), ?a("used"), ?a("slots")
DIRECTIVE STATUS "id" SET ?statusCheck a specific agent's online status
DIRECTIVE "id" READ "path"Read a file under the client root — returns data
DIRECTIVE "id" WRITE "path" CONTENT "..."Write a file under the client root
DIRECTIVE "id" MOUSE "[Token]"Button, scroll or relative move, e.g. "[MouseDown]+[Down]"
DIRECTIVE "id" KEYBOARD "[Key]+[Key]"Send a key or chord, e.g. "[Control]+[c]"
DIRECTIVE "id" POINTER X n Y nMove the pointer to absolute coordinates
DIRECTIVE "id" POINTER COORDINATES SET ?cRead current pointer position — ?c("x"), ?c("y")