Process plugins
A process plugin is an executable the app starts and talks to over stdin and stdout. Write it in any language; the reference plugin, Checksum, is about ninety lines of Swift in PluginSDK/checksum.
Build and install
The manifest says "runtime": "process" and names the executable in entry. The executable sits beside the manifest, with the executable bit set:
cd PluginSDK/checksum
swift build -c release
mkdir -p ~/Library/Application\ Support/OcelotFM/Plugins/checksum
cp manifest.json .build/release/checksum ~/Library/Application\ Support/OcelotFM/Plugins/checksum/
Then Settings ▸ Plugins ▸ Reload All, and enable it.
Lifecycle
The worker starts at the first command or column call, inside the plugin's sandbox (see Permissions), with the plugin's folder as its read-only working directory and a minimal environment: a fixed PATH, HOME, USER, LANG, LC_ALL, SHELL, TMPDIR (a folder of its own) and OCELOT_PLUGIN_DATA (its persistent folder). It stays running between calls and is stopped when the plugin is disabled, reloaded, or the app quits.
A call that runs out of time (10 seconds for a command, 2 for a column batch) ends the worker: SIGTERM to its whole process group, SIGKILL a second later for whatever ignored it. That is a strike; so is a crash, a worker that exits on its own, a malformed message, or a failed initialize. Three strikes disable the plugin. The next call after a stop starts a fresh worker.
Framing
Every message, in both directions, is a header and a JSON body:
Content-Length: 61\r\n
\r\n
{"id":1,"method":"execute","params":{...}}
A message nested deeper than 32 levels, or a stream of headers that never ends in a body, is malformed. Lines on stderr go to the plugin's log.
Messages from the app
| Method | Params | Reply |
|---|---|---|
initialize | {hostAPIVersion, grantedPermissions} | {name, version}; the handshake must succeed before anything else |
execute | {command, context} with context = {selectionPaths, directoryPath, paneID, surface} | null |
columnValues | {column, paths, context} | an object from path to value (see Columns) |
shutdown | — | none: a notification; exit |
A reply is {"id": …, "result": …} or {"id": …, "result": null, "error": "message"}. Reply to a method you do not know with an error rather than silence.
Messages to the app
Notifications, without an id, sent at any time:
| Method | Params | Notes |
|---|---|---|
notify | {title, body} | only while an execute is running; dropped with a log line otherwise. Cut at 4 096 characters, at most 10 a minute |
log | {level, message} | the plugin's log; 100 lines a second at most |
invalidateColumn | {column, paths} | the values of that column changed; paths optional, see Columns |
Storage and tools
The worker writes only to $OCELOT_PLUGIN_DATA and $TMPDIR; /tmp is not writable, so avoid tools that hard-code it. Outbound network needs "network.client" in the manifest and "hostAPI": {"min": "1.3"}. A process plugin runs its own tools itself, inside the same sandbox; the process.spawn permission is for JavaScript plugins.