Skip to main content
Version: Latest

Scripting Solar Sailer

Read this when you want to drive the editor from an external script or tool — any language, no repo checkout, no build step. This is "Tier 0" of the plugin system: an out-of-process script that reads timeline state and posts editing commands over local HTTP. It works against a running editor today. Tier 1 (drop-in module plugins with their own Process-menu entry and settings dialog) is a separate mechanism, covered in the Plugin Development Guide.

Why this is safe to use

Every command a script sends goes through the editor's own command layer — the exact same path the UI, keyboard shortcuts, and the embedded AI agent use. That means validation, link-group cascades, locked-track enforcement, and undo all apply automatically: a script's edit batch lands as one undo step the user can Ctrl+Z, and an invalid batch is rejected atomically (nothing half-applies). A script cannot corrupt a timeline any harder than a UI user can.

Do NOT edit .sailer project files on disk from a script — always go through the running editor's HTTP API. The file format is an internal serialization, and the running editor's state is the source of truth.

Connecting

The server listens on localhost only. Two values identify this launch:

  • Server addresshttp://127.0.0.1:<port>. The port is chosen fresh at every app start.
  • API token — a bearer token, also minted fresh at every app start.

Get both from Edit → Preferences… → Scripting, which shows the current address and has copy buttons for both values. A copied token dies when the app restarts — copy a fresh one each session.

Every /timeline/* request (and the other authed routes) needs the header:

Authorization: Bearer <token>

(Scripts running inside the editor's embedded agent terminal get PROKO_EDITOR_PORT and PROKO_EDITOR_TOKEN as environment variables instead — no copying needed.)

The API in five endpoints

The command surface is self-documenting — start with #1:

  1. GET /agent/instructions — the full generated reference: every command with parameters, descriptions, and workflow tips. Rendered live from the command registry, so it never goes stale.
  2. GET /timeline/summary — clip IDs, positions, media names per track. The cheap read; start here.
  3. GET /timeline/state — full clip detail (sourceStart/End, volume, transforms, markers, in/out, track lock state). Also GET /timeline/media for the media bin.
  4. POST /timeline/commands — execute a batch of editing commands. Body: { "commands": [{ "name": "...", "params": { ... } }, ...], "undoLabel": "What Ctrl+Z will call this edit" } (undoLabel is required). One batch = one undo step; any invalid command rejects the whole batch with a 400 and an actionable error string.
  5. GET /project/current — which project is open ({path, dataDir, name}); POST /project/open switches projects through the full editor lifecycle.

Analysis data is readable too: GET /transcripts / GET /transcripts/{media_id} (word-level transcripts with retake/rough-cut annotations), plus per-module sidecar endpoints (/face-tracking/sidecar/{media_id}, /motion/sidecar/{media_id}, /umm/sidecar/{media_id}, /characters). The three sidecar endpoints return 404 if that module hasn't run; /characters returns an empty list instead.

Rules of the road

  • All timing is integer frames, not seconds: frames = seconds × frameRate. The frame rate is in the timeline state.
  • Safe retries: a 503/504 response is ambiguous (the edit may have applied). Send an idempotencyKey (any unique string, 1–128 chars) with mutating batches and reuse the same key on the retry — a duplicate key returns the original result instead of applying the edit twice.
  • Check track locks before editing: commands that touch clips on a locked track are rejected with an error naming the track.
  • After a project switch, re-read /timeline/summary before trusting any earlier state or replayed result.
  • Command responses are enriched: createdIds/deletedIds are real store IDs safe to use in follow-up commands; warnings carries non-fatal notes (e.g. a ripple held back by a locked track).

Example

Read the summary, then razor-cut every clip at the 2-second mark (assuming 30 fps — frame 60). splitAllAtFrame is a safe first command to try: if nothing spans that frame it succeeds as a no-op instead of erroring.

TOKEN="<paste from Preferences → Scripting>"
BASE="http://127.0.0.1:<port>"

curl -s -H "Authorization: Bearer $TOKEN" "$BASE/timeline/summary"

curl -s -X POST "$BASE/timeline/commands" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"undoLabel": "Script: split at 2s",
"idempotencyKey": "split-at-2s-001",
"commands": [{ "name": "splitAllAtFrame", "params": { "frame": 60 } }]
}'

The same thing in Python:

import requests

BASE = "http://127.0.0.1:<port>"
HEADERS = {"Authorization": "Bearer <token>"}

summary = requests.get(f"{BASE}/timeline/summary", headers=HEADERS).json()

result = requests.post(
f"{BASE}/timeline/commands",
headers=HEADERS,
json={
"undoLabel": "Script: split at 2s",
"idempotencyKey": "split-at-2s-001",
"commands": [{"name": "splitAllAtFrame", "params": {"frame": 60}}],
},
)
print(result.json())

Two payload rules worth knowing before you graduate to bigger edits: undoLabel is required on every command batch (it's the text Ctrl+Z shows the user), and the range-cut commands (deleteRanges/disableRanges) require each range to carry a non-empty trackIds array naming the tracks it cuts — get track ids from /timeline/summary. Consult GET /agent/instructions for the exact parameter shape of every command — it is the authoritative reference, and it evolves with the app.

What a script can and cannot do (v1)

Can: everything the 48-command surface covers — insert/move/trim/split/slip clips, delete/disable ranges, close gaps, copy/paste, markers, track management, link groups, selection, in/out, clip transforms (position/scale/rotation/opacity/crop), undo/redo — plus read transcripts and analysis sidecars, place existing bin media on the timeline, and import new files into the media bin: POST /media/import with {"paths": ["C:/absolute/path.mp4", ...]} (token-authed like every media write route). The response is {media, skipped, failed}skipped names already-imported duplicates, failed carries a {path, reason} per unimportable path — and the running editor's bin updates live with thumbnails/waveform/classification queued automatically, exactly as if the user had used File-dialog or drag-and-drop import.

Cannot (yet): add rendering effects/transitions (the editor's effects system doesn't exist yet; when it lands, scripts inherit it as new commands), or drive the embedded chat agent.

Security posture

The token is machine-scoped and per-launch: anyone with it has full edit control of the running editor, so treat it like a password (don't commit it, don't send it anywhere). The server binds to localhost only. This trust model is deliberate for team tooling; per-plugin scoped tokens are part of the future public plugin system, not Tier 0.