app
Premiere Pro Bridge
Public Made by Adomby adom
Let the AI edit video in Adobe Premiere Pro on your desktop (import, cut, trim, razor, effects, audio, previews, export) with no mouse or keyboard. Check adom-bridge bridge_list for 'premiere'; install with bridge_install + adom-wiki pkg install adom/premiere-bridge, then start with premiere_status.
main
anastasia
Premiere Pro Bridge 0.1.0: Rust bridge, UXP plugin, CEP helper, skills, macOS manifest
32fb583
10d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
//! The verb catalog: one table that drives routing (which in-app runtime runs a verb),
//! timeouts, `premiere_describe`, and the `_hint` / `_next` / `related` / `pitfalls`
//! stamped on every response. The calling AI reads verb OUTPUT, not docs — so the
//! guidance lives here, next to the verb.
use serde_json::{json, Value};
#[derive(Clone, Copy, PartialEq)]
pub enum Runtime {
/// Answered by the Rust bridge itself (no Premiere needed).
Local,
/// The UXP plugin (premierepro API) — the main surface.
Uxp,
/// The invisible CEP/ExtendScript helper (QE DOM) — razor, speed, tracks.
Cep,
/// CEP when connected (true razor), else the UXP emulation.
PreferCep,
}
use Runtime::*;
pub struct Verb {
pub name: &'static str,
pub runtime: Runtime,
pub timeout_sec: u64,
pub summary: &'static str,
pub args: &'static str,
pub hint: &'static str,
pub next: &'static [&'static str],
pub pitfalls: &'static [&'static str],
}
const fn v(
name: &'static str,
runtime: Runtime,
timeout_sec: u64,
summary: &'static str,
args: &'static str,
hint: &'static str,
next: &'static [&'static str],
pitfalls: &'static [&'static str],
) -> Verb {
Verb { name, runtime, timeout_sec, summary, args, hint, next, pitfalls }
}
const CLIP_REF: &str = "Clip refs are \"V1:0\" / \"A2:3\" = track:index (0-based, in time order) exactly as premiere_get_sequence lists them. Indexes shift after edits — re-read the sequence before the next edit.";
pub static VERBS: &[Verb] = &[
// ── lifecycle (local) ──
v("describe", Local, 5, "Full machine-readable verb catalog.", "{}", "Pick verbs from here; premiere_eval covers anything not listed.", &["premiere_status"], &[]),
v("status", Local, 15, "Bridge, Premiere, and plugin connection status plus a live ping.", "{}",
"If pluginConnected is false follow the _hint. When connected, premiere_get_project / premiere_get_sequence show what's open.",
&["premiere_get_project", "premiere_get_sequence"], &["status is cheap but pings Premiere; for a no-side-effect readiness check use premiere_readiness."]),
v("readiness", Local, 5, "Read-only readiness: never launches or installs.", "{}",
"state: ready | needs-plugin (→ premiere_install_plugin) | app-not-running (→ premiere_launch) | waiting-for-plugin (→ premiere_wait_for_plugin) | no-app.",
&["premiere_install_plugin", "premiere_launch", "premiere_wait_for_plugin"], &[]),
v("install_plugin", Local, 180, "Install/refresh the Adom UXP plugin (and the invisible CEP helper) into Premiere. No user clicks.", "{force?:bool, cep?:bool=true}",
"If Premiere was running during install, restart it (premiere_quit {save:true} then premiere_launch) so it loads the plugin. The UXP panel must be opened ONCE (Window > UXP Plugins > Adom Bridge); after that Premiere restores it every launch.",
&["premiere_quit", "premiere_launch", "premiere_wait_for_plugin"], &["Installing over a running Premiere doesn't hot-load — restart it."]),
v("plugin_info", Local, 5, "Installed vs bundled plugin versions and install locations.", "{}", "If installedVersion < bundledVersion run premiere_install_plugin.", &["premiere_install_plugin"], &[]),
v("launch", Local, 180, "Start Premiere Pro (optionally opening a .prproj) and wait for the plugin.", "{project?:path, year?:2026, foreground?:true, wait?:true, timeoutSec?:120}",
"When pluginConnected comes back true you're ready. If it times out, follow the _hint (usually: open the Adom Bridge panel once).",
&["premiere_get_project", "premiere_open_project"], &["A fresh Premiere shows the Home screen; open or create a project before timeline verbs."]),
v("quit", Local, 90, "Quit Premiere (optionally saving first).", "{save?:bool, force?:bool}", "Pass save:true to avoid losing edits; Premiere may otherwise show a save prompt.", &["premiere_launch"], &["Unsaved changes can block quit with a dialog; use save:true."]),
v("wait_for_plugin", Local, 600, "Block until the UXP plugin connects.", "{timeoutSec?:90}", "Once connected, run your edit verbs.", &["premiere_get_project"], &[]),
v("events", Local, 5, "Events pushed by the plugin (export progress/complete/error, project opened/dirty).", "{sinceSeq?:0}", "Poll with the returned lastSeq to see only new events.", &["premiere_export_sequence"], &[]),
v("read_file_base64", Local, 30, "Return a desktop file (e.g. a preview PNG) as base64.", "{path, maxBytes?}", "Decode and view the image. For big files use adom-bridge pull_file instead.", &[], &[]),
v("preview_frame", Local, 60, "Render the sequence frame at a time to PNG and return it inline (base64) — the fastest way to SEE the edit.", "{time?:seconds|timecode (default playhead), width?:960, sequence?, inline?:true}",
"Look at the image before telling the user the edit is right. For an overview of the whole cut use premiere_contact_sheet.",
&["premiere_contact_sheet", "premiere_get_sequence"], &["Frames render at the sequence's current state — re-preview after each edit.", "Premiere's frame exporter does NOT evaluate keyframes between two keys (a fade shows as before/after only) — use premiere_monitor_snapshot to check mid-animation frames."]),
v("contact_sheet", Local, 300, "Export N evenly spaced frames across the sequence (or a range) — a storyboard of the cut.", "{count?:8, start?, end?, width?:480, inline?:true}",
"Review the frames in order to judge pacing and continuity.", &["premiere_preview_frame"], &["Keep count <= 16 with inline:true to keep responses small."]),
v("monitor_snapshot", Local, 60, "Read-only capture of Premiere's window (Program Monitor + timeline) via Adom Bridge — no clicks, no focus change. Optionally moves the playhead first.", "{time?, sequence?, settleMs?:700}",
"Use when you need to see exactly what Premiere shows (animated opacity/position mid-keyframe, titles, overlays).", &["premiere_preview_frame"], &["Shows the whole window; the Program Monitor is top-center in the default Editing workspace."]),
v("list_export_presets", Local, 30, "Find .epr export presets on this machine (Premiere + Media Encoder system and user presets).", "{search?:\"h.264\"}",
"Pass a presetPath to premiere_export_sequence.", &["premiere_export_sequence"], &[]),
// ── project ──
v("ping", Uxp, 10, "Round-trip to the plugin.", "{}", "Plugin is alive.", &["premiere_get_project"], &[]),
v("get_project", Uxp, 30, "Open project: name, path, sequences, bins.", "{}", "premiere_list_project_items shows media; premiere_get_sequence shows the timeline.", &["premiere_list_project_items", "premiere_get_sequence"], &[]),
v("open_project", Uxp, 180, "Open a .prproj (dialogs suppressed).", "{path}", "Then premiere_get_project.", &["premiere_get_project", "premiere_list_sequences"], &["Paths are on the DESKTOP machine, not the container."]),
v("new_project", Uxp, 60, "Create a new .prproj.", "{path}", "A new project has no sequence: premiere_import_media then premiere_create_sequence {fromItems:[…]}.", &["premiere_import_media", "premiere_create_sequence"], &["The folder must exist."]),
v("save_project", Uxp, 120, "Save (or Save As with path).", "{path?}", "Saved.", &[], &[]),
v("close_project", Uxp, 60, "Close the project.", "{save?:bool}", "", &["premiere_open_project"], &["Without save:true unsaved edits are discarded."]),
// ── project panel ──
v("list_project_items", Uxp, 60, "List bins/clips/sequences in the Project panel with ids, bins, media paths, durations.", "{bin?, type?:clip|bin|…, search?, limit?:500, details?:true}",
"Use an item's id (or exact name) as `item` in insert/overwrite/append.", &["premiere_insert_clip", "premiere_append_clip"], &[]),
v("import_media", Uxp, 300, "Import files (video/audio/stills/image sequences) into the project.", "{paths:[abs desktop paths], bin?, asNumberedStills?}",
"The returned items[].id are ready for premiere_append_clip / premiere_insert_clip. Files in the container must first be sent with adom-bridge send_files.",
&["premiere_append_clip", "premiere_create_sequence"], &["Paths must exist on the desktop machine."]),
v("create_bin", Uxp, 30, "Create a bin.", "{name, parent?}", "", &["premiere_move_item"], &[]),
v("move_item", Uxp, 30, "Move a project item into a bin.", "{item, bin}", "", &[], &[]),
v("rename_item", Uxp, 30, "Rename a project item.", "{item, name}", "", &[], &[]),
v("delete_item", Uxp, 30, "Remove an item from the project.", "{item}", "", &[], &["Clips using it on timelines go offline/empty — check first."]),
v("set_item_color", Uxp, 30, "Set an item's label color.", "{item, colorIndex:0-15}", "", &[], &[]),
v("set_source_in_out", Uxp, 30, "Set/clear a source clip's in/out points (what gets inserted).", "{item, in?, out?, clear?}", "Then insert/overwrite uses just that range.", &["premiere_insert_clip"], &[]),
v("create_subclip", Uxp, 30, "Make a subclip from a source range.", "{item, start, end, name?, video?, audio?}", "", &["premiere_list_project_items"], &[]),
v("get_metadata", Uxp, 30, "Project + XMP metadata for an item.", "{item, xmp?:bool}", "", &[], &[]),
v("set_xmp_metadata", Uxp, 30, "Write XMP metadata for an item.", "{item, xmp}", "", &[], &[]),
v("transcribe", Uxp, 60, "Start Speech-to-Text on a clip.", "{item, language?:\"en-us\"}", "Transcription runs in the background; poll premiere_get_transcript.", &["premiere_get_transcript"], &[]),
v("get_transcript", Uxp, 60, "Get a clip's transcript JSON (words + timings).", "{item}", "Use word timings to find cut points.", &["premiere_razor", "premiere_add_marker"], &[]),
// ── sequences ──
v("list_sequences", Uxp, 30, "All sequences with guid, duration, which is active.", "{}", "", &["premiere_set_active_sequence", "premiere_get_sequence"], &[]),
v("set_active_sequence", Uxp, 30, "Open + activate a sequence.", "{sequence: name|guid}", "", &["premiere_get_sequence"], &[]),
v("create_sequence", Uxp, 60, "New sequence — from clips (matches their settings), a preset, or defaults.", "{name, fromItems?:[item…], presetPath?, bin?}",
"fromItems is the best way to get matching resolution/fps.", &["premiere_get_sequence", "premiere_append_clip"], &[]),
v("duplicate_sequence", Uxp, 30, "Clone a sequence.", "{sequence?}", "", &["premiere_list_sequences"], &[]),
v("delete_sequence", Uxp, 30, "Delete a sequence.", "{sequence}", "", &[], &[]),
v("nest_selection", Uxp, 30, "Nest the selected clips into a new sequence.", "{sequence?}", "Select first with premiere_select_clips.", &["premiere_select_clips"], &[]),
v("get_sequence", Uxp, 60, "THE timeline view: every track and clip with refs, start/end, source in/out, effects, media.", "{sequence?, detail?:true}",
"Use the clips' `ref` values in edit verbs. Re-read after every edit — indexes shift.", &["premiere_preview_frame", "premiere_trim_clip", "premiere_move_clip"], &[CLIP_REF]),
v("get_sequence_settings", Uxp, 30, "Frame size, fps, PAR, audio settings.", "{sequence?}", "", &["premiere_set_sequence_settings"], &[]),
v("set_sequence_settings", Uxp, 30, "Change frame size / fps / render quality.", "{sequence?, width?, height?, fps?, maxBitDepth?, maxRenderQuality?}", "", &[], &[]),
v("set_playhead", Uxp, 15, "Move the playhead (CTI).", "{time, sequence?}", "premiere_preview_frame with no time shows this frame.", &["premiere_preview_frame"], &[]),
v("set_in_out", Uxp, 15, "Set sequence in/out (and optionally the work area).", "{in?, out?, workArea?:bool, sequence?}", "export_sequence {range:\"inout\"} renders just this.", &["premiere_export_sequence"], &[]),
// ── editing ──
v("insert_clip", Uxp, 60, "Insert a project item at a time (ripples later clips right).", "{item, time?(default playhead), track?|videoTrack?:\"V1\", audioTrack?:\"A1\", limitShift?}",
"Check placement with premiere_get_sequence, then premiere_preview_frame.", &["premiere_get_sequence", "premiere_preview_frame"], &["Insert shifts everything after `time` on the targeted tracks."]),
v("overwrite_clip", Uxp, 60, "Overwrite a project item onto the timeline at a time.", "{item, time?, track?|videoTrack?, audioTrack?}", "", &["premiere_get_sequence"], &["Replaces whatever was there."]),
v("append_clip", Uxp, 60, "Place a project item at the end of a track.", "{item, track?:\"V1\", audioTrack?}", "Repeat to build a rough cut in order.", &["premiere_get_sequence", "premiere_contact_sheet"], &[]),
v("move_clip", Uxp, 30, "Move clip(s) to a new start time or by an offset.", "{clip|clips, to?|by?, sequence?}", "", &["premiere_get_sequence"], &[CLIP_REF, "Moving onto another clip overwrites it."]),
v("trim_clip", Uxp, 30, "Trim a clip: set start/end on the timeline, or trimStart/trimEnd by seconds, or source in/out.", "{clip, start?, end?, trimStart?, trimEnd?, inPoint?, outPoint?}",
"Trimming leaves a gap; premiere_remove_gap closes it.", &["premiere_remove_gap", "premiere_get_sequence"], &[CLIP_REF]),
v("remove_clips", Uxp, 30, "Delete clips, optionally ripple-deleting (closing the gap).", "{clip|clips, ripple?:false}", "", &["premiere_get_sequence"], &[CLIP_REF, "Linked audio isn't removed unless you include its A-track ref too."]),
v("remove_gap", Uxp, 30, "Close the gap before a clip (shift it and later clips on that track left).", "{clip}", "", &["premiere_get_sequence"], &["Only that track moves — do the matching A-track too to keep sync."]),
v("razor", PreferCep, 60, "Cut clips at a time (all tracks, or the ones you list).", "{time, tracks?:[\"V1\",\"A1\"], sequence?}",
"After cutting, premiere_get_sequence shows the two halves; remove_clips {ripple:true} the unwanted piece.", &["premiere_get_sequence", "premiere_remove_clips"],
&["Uses the CEP helper's true razor when it's connected; otherwise a trim+clone emulation (method:\"uxp-emulated\") — verify with premiere_get_sequence.", "CEP razor acts on the ACTIVE sequence."]),
v("set_clip_enabled", Uxp, 30, "Enable/disable clip(s).", "{clip|clips, enabled:bool}", "", &[], &[]),
v("rename_clip", Uxp, 30, "Rename a timeline clip.", "{clip, name}", "", &[], &[]),
v("duplicate_clip", Uxp, 30, "Clone a timeline clip by a time offset and/or onto another track.", "{clip, offset?:seconds, videoTrackOffset?, audioTrackOffset?, insert?}", "", &["premiere_get_sequence"], &[]),
v("select_clips", Uxp, 15, "Select clips in the timeline.", "{clip|clips}", "", &["premiere_nest_selection", "premiere_scene_edit_detection"], &[]),
v("get_selection", Uxp, 15, "What's selected in the timeline.", "{}", "", &[], &[]),
v("clear_selection", Uxp, 15, "Deselect all.", "{}", "", &[], &[]),
v("scene_edit_detection", Uxp, 120, "Auto-detect scene cuts in clips (apply cuts, markers, or subclips).", "{clip|clips, operation?:cut|marker|subclip}", "Runs async in Premiere; re-read the sequence in a few seconds.", &["premiere_get_sequence"], &[]),
v("set_speed", Cep, 60, "Change clip speed (and reverse / ripple / keep pitch).", "{clip:\"V1:0\", speed:0.5|2.0, reverse?, ripple?, maintainPitch?:true}",
"Needs the CEP helper. Re-read the sequence: duration changes.", &["premiere_get_sequence"], &["Acts on the ACTIVE sequence."]),
v("add_tracks", Cep, 30, "Add video and/or audio tracks.", "{video?:n, audio?:n, audioType?:stereo|mono|5.1}", "", &["premiere_get_sequence"], &["Needs the CEP helper; acts on the active sequence."]),
v("remove_track", Cep, 30, "Delete a track.", "{track:\"V3\"}", "", &[], &["Needs the CEP helper."]),
v("lock_track", Cep, 15, "Lock/unlock a track.", "{track, locked?:true}", "", &[], &["Needs the CEP helper."]),
v("set_track_target", Cep, 15, "Target/untarget a track for edits.", "{track, targeted?:true}", "", &[], &["Needs the CEP helper."]),
v("set_track_mute", Uxp, 15, "Mute/unmute a track.", "{track:\"A2\", mute?:true}", "", &[], &[]),
v("rename_track", Uxp, 15, "Rename a track.", "{track, name}", "", &[], &[]),
// ── effects ──
v("list_effects", Uxp, 30, "Available video effects, audio effects, or video transitions.", "{kind?:video|audio|transition, search?}", "Use matchName (video/transition) or displayName (audio) in add_effect / add_transition.", &["premiere_add_effect", "premiere_add_transition"], &[]),
v("get_clip_effects", Uxp, 30, "A clip's components (Motion, Opacity, Volume, added effects) with every param's value and keyframes.", "{clip, time?}",
"Use component + param names from here in premiere_set_param / premiere_add_keyframes.", &["premiere_set_param", "premiere_add_keyframes"], &[CLIP_REF]),
v("add_effect", Uxp, 30, "Apply a video or audio effect to clip(s).", "{clip|clips, matchName?|name?, index?}", "Then premiere_get_clip_effects to see its params.", &["premiere_get_clip_effects", "premiere_set_param"], &["Audio effects need an A-track clip; video effects a V-track clip."]),
v("remove_effect", Uxp, 30, "Remove a component from a clip.", "{clip, component: index|name|matchName}", "", &[], &[]),
v("set_param", Uxp, 30, "Set an effect parameter (static, or a keyframe when `time` is given).", "{clip|clips, component, param, value (number|bool|[x,y]|{red,green,blue,alpha}), time?}",
"preview_frame to check the look.", &["premiere_preview_frame", "premiere_get_clip_effects"], &["Read the current value with premiere_get_clip_effects first and keep the same shape/scale (e.g. Motion>Position is [x,y])."]),
v("add_keyframes", Uxp, 30, "Animate a parameter with several keyframes.", "{clip, component, param, keyframes:[{time, value}], interpolation?:linear|hold|bezier}", "", &["premiere_preview_frame"], &["Keyframe times are sequence time."]),
v("remove_keyframes", Uxp, 30, "Remove one keyframe, a range, or all (static again).", "{clip, component, param, time?|start&end?|all?}", "", &[], &[]),
v("set_transform", Uxp, 30, "Convenience: Motion scale/position/rotation/anchor and Opacity in one call.", "{clip, scale?, position?:[x,y], rotation?, anchor?, opacity?, time?}", "", &["premiere_preview_frame"], &[]),
v("add_transition", Uxp, 30, "Add a video transition at a clip's start or end.", "{clip|clips, name?:\"Cross Dissolve\"|matchName?, duration?:1, position?:end|start, singleSided?}",
"", &["premiere_preview_frame"], &["Video only. For audio crossfades use premiere_audio_fade."]),
v("remove_transition", Uxp, 30, "Remove a clip's start/end transition.", "{clip, position?:end|start}", "", &[], &[]),
v("insert_mogrt", Uxp, 60, "Insert a Motion Graphics Template (titles/lower thirds) from a .mogrt path or a CC Library.", "{path?|library&element, time?, track?:\"V2\"}",
"Then get_clip_effects on it to find its text param and set_param to change the text.", &["premiere_get_clip_effects", "premiere_set_param"], &[]),
// ── audio ──
v("set_clip_volume", Uxp, 30, "Set an audio clip's level in dB (or as a keyframe at a time).", "{clip|clips:\"A1:0\", db?:-6, level?:raw, time?}",
"Use premiere_audio_probe to read the current level.", &["premiere_audio_probe", "premiere_audio_fade"], &["Volume lives on A-track clips, not the linked video clip."]),
v("audio_fade", Uxp, 30, "Fade an audio clip in or out with Volume keyframes.", "{clip:\"A1:0\", type?:in|out, duration?:1, db?:0}", "", &["premiere_audio_probe"], &[]),
v("audio_probe", Uxp, 15, "Read an audio clip's raw Volume level + dB estimate.", "{clip}", "", &["premiere_set_clip_volume"], &[]),
// ── markers ──
v("list_markers", Uxp, 30, "Sequence (or clip) markers.", "{sequence?|item?}", "", &["premiere_add_marker"], &[]),
v("add_marker", Uxp, 30, "Add a marker (comment/chapter/weblink) with name, comments, duration, color.", "{time?, name?, comments?, duration?, type?, colorIndex?, item?}", "", &["premiere_list_markers"], &[]),
v("remove_marker", Uxp, 30, "Remove markers by guid, name, or all.", "{guid?|name?|all?}", "", &[], &[]),
// ── preview / export ──
v("export_frame", Uxp, 60, "Write the frame at a time to an image file on the desktop.", "{time?, dir?, name?, format?:png|jpg|tif, width?, height?}", "premiere_preview_frame does this AND returns the image inline.", &["premiere_preview_frame", "premiere_read_file_base64"], &[]),
v("export_frames", Uxp, 300, "Write several frames (times[] or count evenly spaced).", "{times?:[…], count?, start?, end?, dir?, width?}", "", &["premiere_contact_sheet"], &[]),
v("export_sequence", Uxp, 300, "Render the sequence to a file with an .epr preset (immediately, or queued in Media Encoder).", "{output:abs path, preset:.epr path, mode?:immediate|ame|queue, range?:full|inout}",
"mode:immediate renders inside Premiere and returns when the file is written; mode:ame queues to Media Encoder (watch premiere_events). Then adom-bridge pull_file the output to inspect it.", &["premiere_events", "premiere_list_export_presets"], &["Set premiere_set_in_out first and pass range:\"inout\" to render only part.", "Long immediate renders can exceed the HTTP budget — pass timeoutSec or use mode:\"ame\"."]),
v("export_interchange", Uxp, 120, "Export the sequence as OpenTimelineIO, FCP XML, or AAF.", "{output, format?:otio|fcpxml|aaf}", "", &[], &[]),
v("source_monitor", Uxp, 30, "Open a clip/file in the Source Monitor, seek, play, or close.", "{item?|path?, time?, play?, speed?, close?}", "", &["premiere_set_source_in_out"], &[]),
// ── escape hatches ──
v("eval", Uxp, 600, "Run arbitrary async JS inside Premiere's UXP plugin with the full premierepro API.", "{code: async-function body; in scope: ppro, h (helpers: findClip, toTick, tx, …), args, require}",
"Use for anything no verb covers. Edits must go through h.tx(project, label, () => [actions]) (built synchronously).",
&["premiere_get_sequence"], &["Return JSON-able values.", "create*Action calls only work inside h.tx (lockedAccess)."]),
v("extendscript", Cep, 600, "Run ExtendScript (incl. the QE DOM) via the CEP helper.", "{code: function body (ES3), args?}", "For legacy-only features (QE). Prefer UXP verbs.", &[], &["ES3: no let/const/arrow; return plain values."]),
v("export_frame_qe", Cep, 60, "Export a frame via QE (fallback when Exporter.exportSequenceFrame misbehaves).", "{time, path}", "", &["premiere_read_file_base64"], &[]),
];
pub fn find(bare: &str) -> Option<&'static Verb> {
VERBS.iter().find(|v| v.name == bare && v.runtime != Local)
}
pub fn get(bare: &str) -> Option<&'static Verb> {
VERBS.iter().find(|v| v.name == bare)
}
fn prefixed(names: &[&str]) -> Vec<String> {
names.iter().map(|n| n.to_string()).collect()
}
/// Stamp guidance onto a response. Never overwrites a `_hint` the verb wrote itself.
pub fn decorate(bare: &str, out: &mut Value) {
let Some(spec) = get(bare) else { return };
let ok = out["success"].as_bool().unwrap_or(false);
if !out.is_object() {
return;
}
if ok {
if out.get("_hint").is_none() && !spec.hint.is_empty() {
out["_hint"] = json!(spec.hint);
}
if out.get("_next").is_none() && !spec.next.is_empty() {
out["_next"] = json!(prefixed(spec.next));
}
} else if out.get("_hint").is_none() {
out["_hint"] = json!(error_hint(out["errorCode"].as_str().unwrap_or("")));
}
if !spec.pitfalls.is_empty() && out.get("pitfalls").is_none() {
out["pitfalls"] = json!(spec.pitfalls);
}
out["verb"] = json!(format!("premiere_{bare}"));
}
fn error_hint(code: &str) -> &'static str {
match code {
"no_project" => "No project open: premiere_open_project {path} or premiere_new_project {path}.",
"no_sequence" => "No active sequence: premiere_list_sequences then premiere_set_active_sequence, or premiere_create_sequence.",
"clip_not_found" | "bad_clip" => "Re-read the timeline with premiere_get_sequence and use a `ref` from it (\"V1:0\").",
"item_not_found" => "premiere_list_project_items {search:\"…\"} shows item ids/names; import with premiere_import_media first.",
"track_not_found" => "premiere_get_sequence lists the tracks; premiere_add_tracks adds more.",
"component_not_found" | "param_not_found" => "premiere_get_clip_effects lists the clip's components and exact param names.",
"effect_not_found" | "transition_not_found" => "premiere_list_effects {kind, search} lists exact names.",
"transaction_failed" => "Premiere rejected the edit. Read the clip with premiere_get_sequence and check times are inside the media.",
"bad_time" => "Times: seconds (12.5), timecode \"00:00:12:15\", \"300f\" (frames), or {ticks}.",
"not_in_uxp" => "This plugin build lacks that verb — premiere_install_plugin {force:true} then restart Premiere, or use premiere_eval.",
"cep_not_connected" => "The CEP helper isn't connected. premiere_install_plugin (sets it up), then restart Premiere.",
"extendscript_error" => "ExtendScript threw — check the `error` and `line`; QE calls need an active sequence.",
_ => "Read `error`. premiere_status shows connection health; premiere_eval lets you probe the API directly.",
}
}
pub fn describe() -> Value {
let verbs: Vec<Value> = VERBS
.iter()
.map(|v| {
json!({
"name": format!("premiere_{}", v.name),
"summary": v.summary,
"input": v.args,
"runsIn": match v.runtime { Local => "bridge", Uxp => "uxp-plugin", Cep => "cep-helper", PreferCep => "cep-helper|uxp-plugin" },
"timeoutSeconds": v.timeout_sec,
"longRunning": v.timeout_sec > 60,
"statusVerb": "premiere_status",
"hint": v.hint,
"related": prefixed(v.next),
"pitfalls": v.pitfalls,
})
})
.collect();
let catalog = json!({
"bridge": "premiere",
"version": crate::VERSION,
"verbCount": verbs.len(),
"conventions": {
"clipRefs": CLIP_REF,
"times": "seconds (12.5) | timecode \"00:00:12:15\" | \"300f\" | {ticks}",
"tracks": "\"V1\".. / \"A1\".. (1-based like the Premiere UI)",
"paths": "All file paths are on the desktop running Premiere. Move files with adom-bridge send_files / pull_file.",
"undo": "Every edit verb is ONE undo step in Premiere (Edit > Undo) labelled with the action.",
},
"verbs": verbs,
});
json!({"success": true, "data": catalog.clone(), "catalog": catalog})
}