RheoclesREE-oh-kleez

Draft. Written against the spec, ahead of the code. The words are the intent; the software is not there yet.

API reference

Every route, on both transports. 3 generated from docs/openapi.yaml (0.1.0); 12 still only in the spec, marked planned.

Conventions

HTTP + SSEhttp://127.0.0.1:7447
WebSocketws://127.0.0.1:7448
AuthAuthorization: Bearer <token> on every HTTP request; ?access_token= also accepted on GET /events; on the WebSocket, a first frame of { "auth": "<token>" }
BodiesJSON in, JSON out; Content-Type: application/json
Pathsevery file path in every response is relative to outputRoot from GET /
Timeshost times are ISO 8601 UTC with milliseconds; t is seconds from the cue; timecode is HH:MM:SS:FF
Absencean unmeasurable value is absent, or null where the field is always present — never 0
the error shape — error for humans, code for programs
{ "error": "destination ep12 already exists", "code": "conflict" }
statuscodewhen
400bad_requestmalformed JSON, missing body, a frame without method/path
401unauthorizedno token, wrong token, WebSocket before the auth frame
403permission_deniedmacOS has not granted the device class this stream needs
404not_foundno such route, stream or take
405method_not_allowedthe path exists, the method does not
409conflictdestination already exists; a take already active
500internala handler threw
501unsupportedthis kind cannot be captured yet
503device_unavailablethe device is gone, busy, or refused the configuration
507insufficient_storagethe disk pre-flight refused the take

Discovery

GET/pinned

Who this daemon is, where files go, how much room there is.

401
No token, or the wrong one.

Discovery.

over WebSocket
→ {"id":1,"method":"GET","path":"/"}
← {"id":1,"status":200,"body":{"name":"Rheocles","version":"0.1.0","hostname":"lens-macbook-pro.local","machineId":"CD3B7EE5-5E6C-5155-854A-72E4728555F7","outputRoot":"/Users/gopher/Movies/Rheocles","freeBytes":44878079167,"auth":"bearer","ports":{"http":7447,"ws":7448}}}
response fieldtype
name*"Rheocles"
version*string
hostname*string
machineId*stringThe kernel host UUID; stable across renames and reboots.
outputRoot*stringThe one absolute path in the API. Every other path is relative to it.
freeBytesintegerBytes available for important usage on the output root's volume. Absent when unmeasurable — absent means unknown, never zero.
auth*"bearer"
ports*object
ports.http*integer
ports.ws*integer

Streams

Every input on the machine, with a stable id. Arming makes a device live; it never writes.

GET/streamspinned

Every stream with its armed state, plus what macOS lets this process see.

401
No token, or the wrong one.

Streams, in a fixed order — displays, windows, cameras, microphones, system audio.

over WebSocket
→ {"id":2,"method":"GET","path":"/streams"}
← {"id":2,"status":200,"body":{"streams":[],"permissions":{"camera":"authorized","microphone":"authorized","screen":"authorized"}}}
response fieldtype
streams*object[]
streams[].id*stringStable and URL-safe, <kind>:<identifier>. Clients hold this.
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*stringWhat the user calls it.
streams[].model*stringWhat the hardware calls itself, or the owning application for a window.
streams[].capabilities*object
streams[].capabilities.videoobject
streams[].capabilities.audioobject
streams[].armed*booleanWhether the capture session is live.
streams[].activeobjectWhat the device is actually delivering; present only while armed.
streams[].active.videoobject
streams[].active.audioobject
streams[].framesSeenintegerFrames or audio buffers delivered since arming; present only while armed.
permissions*objectWhat macOS has let this process do. Screen Recording gates displays and windows both; with screen anything but authorized the list has none.
permissions.camera*"authorized" | "denied" | "restricted" | "notDetermined"
permissions.microphone*"authorized" | "denied" | "restricted" | "notDetermined"
permissions.screen*"authorized" | "denied" | "restricted" | "notDetermined"

POST/streams/{id}/armpinned

Make a device live (frames flow and are discarded) or release it. Never stamps, never writes.

fieldtype
armed*boolean
400
Malformed or missing JSON body.
401
No token, or the wrong one.
403
macOS has not granted this process the device class.
404
No such stream or take.
503
The device is gone, busy, or refused the configuration.

The stream as it now is. Idempotent in both directions.

over WebSocket
→ {"id":3,"method":"POST","path":"/streams/camera:0x2300000fd9009c/arm","body":{"armed":true}}
← {"id":3,"status":200,"body":{"id":"camera:0x2300000fd9009c","kind":"camera","name":"Elgato 4K X","model":"UVC Camera VendorID_4057 ProductID_156","capabilities":{"video":{"width":3840,"height":2160,"maxFrameRate":30}},"armed":true,"active":{"video":{"width":1280,"height":720,"maxFrameRate":120}},"framesSeen":0}}
response fieldtype
id*stringStable and URL-safe, <kind>:<identifier>. Clients hold this.
kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
name*stringWhat the user calls it.
model*stringWhat the hardware calls itself, or the owning application for a window.
capabilities*object
capabilities.videoobject
capabilities.video.width*integerNative pixels.
capabilities.video.height*integer
capabilities.video.maxFrameRate*numberThe highest advertised rate; the signal's real rate can be lower.
capabilities.audioobject
capabilities.audio.sampleRate*number
capabilities.audio.channels*integer
armed*booleanWhether the capture session is live.
activeobjectWhat the device is actually delivering; present only while armed.
active.videoobject
active.video.width*integer
active.video.height*integer
active.video.maxFrameRate*number
active.audioobject
active.audio.sampleRate*number
active.audio.channels*integer
framesSeenintegerFrames or audio buffers delivered since arming; present only while armed.

Takes

Create reserves the paths and writes the manifest. Start is the cue. Stop finalises.

POST/takesplanned

Snapshot the armed set, reserve every path, write the manifest. Not recording.

fieldtype
namestringNames the take folder. Defaults to a timestamp.
destinationstringA folder, relative to the output root.
filesobjectMap of stream id → relative path, for clients that want to name files.
codec"hevc" | "prores422"The one global codec setting for this take.
expectedDurationnumberSeconds. Used for the disk pre-flight; defaults to 1800.
overwritebooleanAllow a destination that already exists. Otherwise 409.
409
the destination already exists and overwrite is not true; or a take is already active
507
the disk pre-flight refused the take
response
{ "id": "tk_7f3a", "state": "created",
"files": { "camera:0x2300000fd9009c": "ep12/cam-facetime.mov", "microphone:Scarlett_2i2": "ep12/mic-mv7.wav" } }

Every path relative to the output root.

over WebSocket
{ "id": 7, "method": "POST", "path": "/takes", "body": { "name": "ep12" } }

POST/takes/{id}/startplanned

The cue. Every armed stream joins at once.

409
the take is not in state created
response
{ "id": "tk_7f3a", "state": "recording", "started": "2026-09-11T14:02:17.004Z" }
over WebSocket
{ "id": 7, "method": "POST", "path": "/takes/tk_7f3a/start", "body": null }

POST/takes/{id}/stopplanned

Finalise every writer and the manifest.

response
{ "id": "tk_7f3a", "state": "complete", "stopped": "2026-09-11T14:14:40.501Z" }
over WebSocket
{ "id": 7, "method": "POST", "path": "/takes/tk_7f3a/stop", "body": null }

POST/takes/{id}/joinplanned

Start one stream’s writer now. Arms it first if it is cold.

fieldtype
stream*stringThe stream id.
409
the take is not recording, or the stream is already joined
response
{ "id": "tk_7f3a", "stream": "window:11597", "path": "ep12/window-keynote.mov",
"started": "2026-09-11T14:06:17.021Z", "timecode": "14:06:17:00" }
over WebSocket
{ "id": 7, "method": "POST", "path": "/takes/tk_7f3a/join", "body": { "stream": "window:11597" } }

POST/takes/{id}/leaveplanned

Finalise that stream’s file. The stream stays armed; the take continues.

fieldtype
stream*stringThe stream id.
response
{ "id": "tk_7f3a", "stream": "window:11597", "stopped": "2026-09-11T14:08:31.115Z", "frames": 8045 }
over WebSocket
{ "id": 7, "method": "POST", "path": "/takes/tk_7f3a/leave", "body": { "stream": "window:11597" } }

POST/takes/{id}/markersplanned

Append { t, label }. Rheocles stamps t; the label is yours.

fieldtype
label*stringNever interpreted by Rheocles.
409
the take is not recording
response
{ "t": 38.7, "label": "cold-open out" }
over WebSocket
{ "id": 7, "method": "POST", "path": "/takes/tk_7f3a/markers", "body": { "label": "cold-open out" } }

GET/takes/{id}planned

The manifest — live while recording, from disk afterwards.

404
no such take
response
{ "take": { "id": "tk_7f3a", "state": "recording", },
"streams": [ ], "markers": [ ] }

The full shape is on Takes and the manifest.

over WebSocket
{ "id": 7, "method": "GET", "path": "/takes/tk_7f3a", "body": null }

GET/takesplanned

Recent takes.

response
[ { "id": "tk_7f3a", "name": "ep12", "state": "complete",
"created": "2026-09-11T14:02:09.412Z", "path": "ep12" }, ]
over WebSocket
{ "id": 7, "method": "GET", "path": "/takes", "body": null }

Record

POST/recordplanned

Create and start together. The one-click form; what the popover’s button does.

fieldtype
The same body as POST /takes.
response
{ "id": "tk_7f3a", "state": "recording", "files": { } }
over WebSocket
{ "id": 7, "method": "POST", "path": "/record", "body": { "name": "ep12" } }

Events

State, levels, drift, joins, errors — pushed as they happen.

GET/eventsplanned

Server-sent events. One-way; every event type below.

A browser EventSource cannot set an Authorization header. How the token travels on this route from a browser — a query parameter, or fetch with a readable stream — is the reference’s to pin. Native clients set the header.

response
event: level
data: { "stream": "microphone:Scarlett_2i2", "peakDb": -19.4, "ts": 1789178960251 }

Preview

GET/preview/{stream}planned

Low-rate preview frames for one stream, on demand. One at a time.

For a popover, not a monitor wall. Opening a second preview closes the first. The frame format and rate are the reference’s to pin.

404
no such stream
over WebSocket
{ "id": 7, "method": "GET", "path": "/preview/camera:0x2300000fd9009c", "body": null }

WebSocket

Everything above, full duplex, on port 7448.

WS/planned

One socket carries every command and every event.

Text frames, one JSON object each. The first frame must be { "auth": "<token>" }; until then every command answers 401 and no event is delivered. A command frame is the HTTP request as an object — id is anything you like and is echoed back, method, path, and the optional query and body are exactly the HTTP request’s — and the reply carries the HTTP status and body. Events arrive as objects with an event key and no id.

frames
→ { "auth": "<token>" }
← { "id": null, "status": 200, "body": { "authenticated": true } }
→ { "id": 7, "method": "POST", "path": "/takes/tk_7f3a/start", "query": {}, "body": null }
← { "id": 7, "status": 200, "body": { "id": "tk_7f3a", "state": "recording", … } }
← { "event": "state", "take": "tk_7f3a", "state": "recording" }

Event types

Delivered on GET /events as data: lines, and on the WebSocket as objects with an event key and no id. Shapes are planned until the YAML pins them.

eventwhendata
streampinnedA stream's armed state changed.{"event":"stream","stream":{"id":"microphone:BuiltInMicrophoneDevice","kind":"microphone","name":"MacBook Pro Microphone","model":"Digital Mic","capabilities":{"audio":{"sampleRate":48000,"channels":1}},"armed":true,"active":{"audio":{"sampleRate":48000,"channels":1}},"framesSeen":0}}
stateplanneda take changes state{ "take": "tk_7f3a", "state": "recording", "at": "2026-09-11T14:02:17.004Z" }
joinplanneda stream starts writing{ "take": "tk_7f3a", "stream": "window:11597", "t": 240.017, "timecode": "14:06:17:00" }
leaveplanneda stream’s file is finalised{ "take": "tk_7f3a", "stream": "window:11597", "t": 374.111, "frames": 8045 }
levelsplannedan armed audio stream’s meter ticks{ "stream": "microphone:Scarlett_2i2", "peakDb": -19.4 }
driftplanneda recording stream’s drift is re-measured{ "take": "tk_7f3a", "stream": "camera:0x2300000fd9009c", "driftMs": -3, "frames": 21540 }
markerplanneda marker lands{ "take": "tk_7f3a", "t": 38.7, "label": "cold-open out" }
errorplannedsomething fails; the take may now be incomplete{ "take": "tk_7f3a", "stream": "display:56A96CFC-7F21", "reason": "disk full", "state": "incomplete" }