Skip to main content
Version: 0.4.0

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 address — http://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.