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.
← Commit history
Premiere Pro Bridge 0.1.0: Rust bridge, UXP plugin, CEP helper, skills, macOS manifest
37 files changed
+5484−1
.gitignore+3Cargo.lock+932Cargo.toml+27README.md+57−1SKILL.md+13adom-bridge-premiere-manifest-macos.json+13bridge.json+193dev-skills/premiere-bridge-dev/SKILL.md+58dev-skills/premiere-bridge-publish/SKILL.md+44install.sh+15launcher.py+41package.json+69plugin/cep/CSXS/manifest.xml+48plugin/cep/host.jsx+173plugin/cep/index.html+7plugin/cep/main.js+90plugin/uxp/icons/[email protected]plugin/uxp/icons/[email protected]plugin/uxp/icons/[email protected]plugin/uxp/icons/[email protected]plugin/uxp/index.html+21plugin/uxp/index.js+1700plugin/uxp/manifest.json+32scripts/build-release.sh+15scripts/dev-sync.sh+21scripts/pb.sh+8scripts/seq.sh+8scripts/sync-verbs.py+10skills/premiere-bridge/SKILL.md+71skills/premiere-editing/SKILL.md+78skills/premiere-preview-export/SKILL.md+35src/ab.rs+63src/catalog.rs+276src/host.rs+671src/main.rs+479src/plugin_pkg.rs+205uninstall.sh+8.gitignoreadded+3@@ -0,0 +1,3 @@+target/+dist/+*.ccx
Cargo.lockadded+932@@ -0,0 +1,932 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "adler2"+version = "2.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"++[[package]]+name = "adom-bridge-premiere"+version = "0.1.0"+dependencies = [+ "axum",+ "base64",+ "futures-util",+ "rand",+ "serde",+ "serde_json",+ "tokio",+ "zip",+]++[[package]]+name = "arbitrary"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"+dependencies = [+ "derive_arbitrary",+]++[[package]]+name = "async-trait"+version = "0.1.92"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.6",+]++[[package]]+name = "atomic-waker"+version = "1.1.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"++[[package]]+name = "axum"+version = "0.7.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"+dependencies = [+ "async-trait",+ "axum-core",+ "base64",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "http-body-util",+ "hyper",+ "hyper-util",+ "itoa",+ "matchit",+ "memchr",+ "mime",+ "percent-encoding",+ "pin-project-lite",+ "rustversion",+ "serde",+ "serde_json",+ "serde_path_to_error",+ "serde_urlencoded",+ "sha1",+ "sync_wrapper",+ "tokio",+ "tokio-tungstenite",+ "tower",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "axum-core"+version = "0.4.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"+dependencies = [+ "async-trait",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "http-body-util",+ "mime",+ "pin-project-lite",+ "rustversion",+ "sync_wrapper",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "base64"+version = "0.22.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"++[[package]]+name = "block-buffer"+version = "0.10.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"+dependencies = [+ "generic-array",+]++[[package]]+name = "bumpalo"+version = "3.20.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"++[[package]]+name = "byteorder"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"++[[package]]+name = "bytes"+version = "1.12.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"++[[package]]+name = "cfg-if"+version = "1.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"++[[package]]+name = "cpufeatures"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"+dependencies = [+ "libc",+]++[[package]]+name = "crc32fast"+version = "1.5.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"+dependencies = [+ "cfg-if",+]++[[package]]+name = "crossbeam-utils"+version = "0.8.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"++[[package]]+name = "crypto-common"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"+dependencies = [+ "generic-array",+ "typenum",+]++[[package]]+name = "data-encoding"+version = "2.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"++[[package]]+name = "derive_arbitrary"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.6",+]++[[package]]+name = "digest"+version = "0.10.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"+dependencies = [+ "block-buffer",+ "crypto-common",+]++[[package]]+name = "displaydoc"+version = "0.2.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.6",+]++[[package]]+name = "equivalent"+version = "1.0.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"++[[package]]+name = "errno"+version = "0.3.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"+dependencies = [+ "libc",+ "windows-sys",+]++[[package]]+name = "flate2"+version = "1.1.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"+dependencies = [+ "crc32fast",+ "miniz_oxide",+]++[[package]]+name = "form_urlencoded"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"+dependencies = [+ "percent-encoding",+]++[[package]]+name = "futures-core"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"++[[package]]+name = "futures-sink"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"++[[package]]+name = "futures-task"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"++[[package]]+name = "futures-util"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"+dependencies = [+ "futures-core",+ "futures-sink",+ "futures-task",+ "pin-project-lite",+ "slab",+]++[[package]]+name = "generic-array"+version = "0.14.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"+dependencies = [+ "typenum",+ "version_check",+]++[[package]]+name = "getrandom"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"+dependencies = [+ "cfg-if",+ "libc",+ "wasi",+]++[[package]]+name = "hashbrown"+version = "0.17.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"++[[package]]+name = "http"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"+dependencies = [+ "bytes",+ "itoa",+]++[[package]]+name = "http-body"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"+dependencies = [+ "bytes",+ "http",+]++[[package]]+name = "http-body-util"+version = "0.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"+dependencies = [+ "bytes",+ "futures-core",+ "http",+ "http-body",+ "pin-project-lite",+]++[[package]]+name = "httparse"+version = "1.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"++[[package]]+name = "httpdate"+version = "1.0.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"++[[package]]+name = "hyper"+version = "1.12.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"+dependencies = [+ "atomic-waker",+ "bytes",+ "futures-core",+ "http",+ "http-body",+ "httparse",+ "httpdate",+ "itoa",+ "pin-project-lite",+ "smallvec",+ "tokio",+]++[[package]]+name = "hyper-util"+version = "0.1.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"+dependencies = [+ "bytes",+ "http",+ "http-body",+ "hyper",+ "pin-project-lite",+ "tokio",+ "tower-service",+]++[[package]]+name = "indexmap"+version = "2.14.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"+dependencies = [+ "equivalent",+ "hashbrown",+]++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "libc"+version = "0.2.190"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"++[[package]]+name = "log"+version = "0.4.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"++[[package]]+name = "matchit"+version = "0.7.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"++[[package]]+name = "memchr"+version = "2.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"++[[package]]+name = "mime"+version = "0.3.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"++[[package]]+name = "miniz_oxide"+version = "0.9.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"+dependencies = [+ "adler2",+ "simd-adler32",+]++[[package]]+name = "mio"+version = "1.2.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"+dependencies = [+ "libc",+ "wasi",+ "windows-sys",+]++[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"++[[package]]+name = "percent-encoding"+version = "2.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"++[[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++[[package]]+name = "ppv-lite86"+version = "0.2.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"+dependencies = [+ "zerocopy",+]++[[package]]+name = "proc-macro2"+version = "1.0.107"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quote"+version = "1.0.47"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "rand"+version = "0.8.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"+dependencies = [+ "libc",+ "rand_chacha",+ "rand_core",+]++[[package]]+name = "rand_chacha"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"+dependencies = [+ "ppv-lite86",+ "rand_core",+]++[[package]]+name = "rand_core"+version = "0.6.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"+dependencies = [+ "getrandom",+]++[[package]]+name = "rustversion"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"++[[package]]+name = "ryu"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"++[[package]]+name = "serde"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"+dependencies = [+ "serde_core",+ "serde_derive",+]++[[package]]+name = "serde_core"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.6",+]++[[package]]+name = "serde_json"+version = "1.0.151"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "serde_path_to_error"+version = "0.1.20"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"+dependencies = [+ "itoa",+ "serde",+ "serde_core",+]++[[package]]+name = "serde_urlencoded"+version = "0.7.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"+dependencies = [+ "form_urlencoded",+ "itoa",+ "ryu",+ "serde",+]++[[package]]+name = "sha1"+version = "0.10.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"+dependencies = [+ "cfg-if",+ "cpufeatures",+ "digest",+]++[[package]]+name = "signal-hook-registry"+version = "1.4.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"+dependencies = [+ "errno",+ "libc",+]++[[package]]+name = "simd-adler32"+version = "0.3.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"++[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]]+name = "smallvec"+version = "1.16.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"++[[package]]+name = "socket2"+version = "0.6.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"+dependencies = [+ "libc",+ "windows-sys",+]++[[package]]+name = "syn"+version = "2.0.119"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "syn"+version = "3.0.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "sync_wrapper"+version = "1.0.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"++[[package]]+name = "thiserror"+version = "1.0.69"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"+dependencies = [+ "thiserror-impl 1.0.69",+]++[[package]]+name = "thiserror"+version = "2.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"+dependencies = [+ "thiserror-impl 2.0.21",+]++[[package]]+name = "thiserror-impl"+version = "1.0.69"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+]++[[package]]+name = "thiserror-impl"+version = "2.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.6",+]++[[package]]+name = "tokio"+version = "1.53.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"+dependencies = [+ "bytes",+ "libc",+ "mio",+ "pin-project-lite",+ "signal-hook-registry",+ "socket2",+ "tokio-macros",+ "windows-sys",+]++[[package]]+name = "tokio-macros"+version = "2.7.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.6",+]++[[package]]+name = "tokio-tungstenite"+version = "0.24.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"+dependencies = [+ "futures-util",+ "log",+ "tokio",+ "tungstenite",+]++[[package]]+name = "tower"+version = "0.5.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"+dependencies = [+ "futures-core",+ "futures-util",+ "pin-project-lite",+ "sync_wrapper",+ "tokio",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "tower-layer"+version = "0.3.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"++[[package]]+name = "tower-service"+version = "0.3.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"++[[package]]+name = "tracing"+version = "0.1.44"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"+dependencies = [+ "log",+ "pin-project-lite",+ "tracing-core",+]++[[package]]+name = "tracing-core"+version = "0.1.36"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"+dependencies = [+ "once_cell",+]++[[package]]+name = "tungstenite"+version = "0.24.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"+dependencies = [+ "byteorder",+ "bytes",+ "data-encoding",+ "http",+ "httparse",+ "log",+ "rand",+ "sha1",+ "thiserror 1.0.69",+ "utf-8",+]++[[package]]+name = "typenum"+version = "1.20.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"++[[package]]+name = "unicode-ident"+version = "1.0.26"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"++[[package]]+name = "utf-8"+version = "0.7.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"++[[package]]+name = "version_check"+version = "0.9.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"++[[package]]+name = "wasi"+version = "0.11.1+wasi-snapshot-preview1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"++[[package]]+name = "windows-link"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"++[[package]]+name = "windows-sys"+version = "0.61.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"+dependencies = [+ "windows-link",+]++[[package]]+name = "zerocopy"+version = "0.8.62"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"+dependencies = [+ "zerocopy-derive",+]++[[package]]+name = "zerocopy-derive"+version = "0.8.62"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+]++[[package]]+name = "zip"+version = "2.4.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"+dependencies = [+ "arbitrary",+ "crc32fast",+ "crossbeam-utils",+ "displaydoc",+ "flate2",+ "indexmap",+ "memchr",+ "thiserror 2.0.21",+ "zopfli",+]++[[package]]+name = "zmij"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"++[[package]]+name = "zopfli"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"+dependencies = [+ "bumpalo",+ "crc32fast",+ "log",+ "simd-adler32",+]
Cargo.tomladded+27@@ -0,0 +1,27 @@+[package]+name = "adom-bridge-premiere"+version = "0.1.0"+edition = "2021"+description = "Adom Bridge bridge for Adobe Premiere Pro: drives Premiere through an in-app UXP plugin, no mouse or keyboard automation"+license = "MIT"+publish = false++[[bin]]+name = "premiere-bridge"+path = "src/main.rs"++[dependencies]+tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time", "process", "fs"] }+axum = { version = "0.7", features = ["ws"] }+serde = { version = "1", features = ["derive"] }+serde_json = "1"+futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }+base64 = "0.22"+zip = { version = "2", default-features = false, features = ["deflate"] }+rand = "0.8"++[profile.release]+opt-level = "s"+lto = true+strip = true+codegen-units = 1
README.md+57−1@@ -1,3 +1,59 @@ # Premiere Pro Bridge -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.+Let the AI in Hydrogen edit video in **Adobe Premiere Pro** on your desktop: import footage, build a cut, razor and ripple-delete, add effects and transitions, set audio levels and fades, drop markers, look at rendered frames, and export. It never moves your mouse, types on your keyboard, or steals focus. Every action goes through Adobe's own scripting API inside Premiere.++> **This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.**++## Install++From any Hydrogen container connected to your desktop:++```bash+# 1. the bridge runtime, on your desktop+adom-bridge bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/premiere-bridge/files/adom-bridge-premiere-manifest-macos.json"}'+# 2. the skills, in this container+adom-wiki pkg install adom/premiere-bridge+# 3. put the plugin inside Premiere (no clicks; Premiere hot-loads it)+adom-bridge premiere_install_plugin+adom-bridge premiere_status+```++Requirements: macOS on Apple Silicon, Premiere Pro **25.6 or newer** (tested on Premiere Pro 2026 / 26.5.2), Adom Bridge 2.x. A Windows build is planned. The code is cross-platform but hasn't been built or tested there yet.++## What the AI can do++| Area | Verbs |+|---|---|+| Projects | open, create, save, close; list/import media; bins, rename, labels, subclips, metadata |+| Sequences | list, create (matching clip settings), duplicate, nest; read the full timeline; playhead, in/out, settings |+| Editing | insert, overwrite, append, move, trim, **razor**, delete / ripple delete, close gaps, enable/disable, duplicate, scene-edit detection |+| Effects | list/add/remove video & audio effects, read and set every parameter, keyframe animation, Motion/Opacity transforms, video transitions, MOGRT titles |+| Audio | clip volume in dB, fades, keyframed levels (ducking), track mute |+| Markers & text | sequence/clip markers, Speech-to-Text transcripts with word timings |+| See the edit | rendered frames inline, contact sheets of the whole cut, a read-only snapshot of the Program Monitor |+| Deliver | export with any .epr preset (H.264, YouTube, ProRes…), in/out ranges, Media Encoder queue, OTIO / FCPXML / AAF |+| Escape hatch | `premiere_eval`: any JavaScript against Premiere's full `premierepro` API |++Linked video and audio stay together: trimming, moving, razoring or deleting a clip applies to its linked partner too. Every edit is a single, labelled undo step in Premiere.++## How it works++```+Hydrogen AI ──adom-bridge──▶ Adom Bridge (desktop) ──HTTP──▶ premiere-bridge (Rust)+ │ loopback WebSocket, token-authenticated+ ▼+ Adom UXP plugin inside Premiere ──▶ premierepro API+ Adom CEP helper (optional) ──▶ ExtendScript / QE+```++- **The Rust bridge** is spawned by Adom Bridge on a stable loopback port. It reports health to the Bridges sidebar and carries both in-app plugins inside its binary.+- **The UXP plugin** connects out to the bridge, because a UXP plugin can't host a server, and runs each verb. `premiere_install_plugin` installs it with Adobe's own installer and Premiere loads it immediately, with no restart or panel to open.+- **The CEP helper** is an invisible ExtendScript extension that loads when Premiere starts. It adds a true QE razor (keeping effects on both halves), clip speed, and adding/removing/locking tracks: things Adobe's UXP API doesn't expose yet. Without it, razor uses an exact UXP fallback.++Security: the bridge binds to 127.0.0.1 only. It rejects non-JSON and non-POST requests (a web page can't drive it). The plugin WebSocket requires a per-machine token stored under `~/.adom/premiere-bridge/`. Installing the CEP helper sets Adobe's per-user `PlayerDebugMode=1`, which lets unsigned CEP extensions load.++## Known limits++- Premiere's frame exporter doesn't evaluate keyframes *between* two keys, so a mid-fade frame shows the before/after state. `premiere_monitor_snapshot` shows what Premiere actually displays.+- UXP has no audio transitions; fade audio with volume keyframes instead (`premiere_audio_fade`).+- Clip speed, adding tracks, and the true razor need the CEP helper, which loads the next time Premiere starts after `premiere_install_plugin`.
SKILL.mdadded+13@@ -0,0 +1,13 @@+---+name: premiere-bridge-pack+description: Skills for driving Adobe Premiere Pro through the Adom Bridge premiere bridge (premiere_* verbs). Edit video timelines, cut/trim/razor, effects, transitions, audio levels, previews, and exports, all from the AI with no mouse or keyboard. Start with the premiere-bridge skill. Trigger words: premiere, premiere pro, adobe premiere, edit my video, video timeline, export video.+user-invocable: true+---++# Premiere bridge skills++- **premiere-bridge** (core): cold-start, the 8 rules, core verbs. Read this first.+- **premiere-editing**: cutting, trimming, effects, keyframes, transitions, audio.+- **premiere-preview-export**: preview frames, contact sheets, monitor snapshots, exports.++This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip (`adom-bridge bridge_install`).
adom-bridge-premiere-manifest-macos.jsonadded+13@@ -0,0 +1,13 @@+{+ "manifest_version": 1,+ "name": "premiere",+ "version": "0.1.0",+ "url": "https://wiki.adom.inc/download/adom/premiere-bridge/0.1.0/adom-bridge-premiere-macos-v0.1.0.zip",+ "sha256": "fa3add2c8755716393350d54f03ea0144f7b0b0965fc9f3c77e8739a5a4fea4f",+ "size": 716430,+ "platform": "darwin-arm64",+ "verbPrefixes": ["premiere_"],+ "healthEndpoint": "/health",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/premiere-bridge/files/adom-bridge-premiere-manifest-macos.json",+ "released_at": "2026-10-10T01:48:00Z"+}
bridge.jsonadded+193@@ -0,0 +1,193 @@+{+ "manifest_version": 1,+ "name": "premiere",+ "displayName": "Adobe Premiere Pro",+ "version": "0.1.0",+ "description": "Drive Adobe Premiere Pro from the AI with no mouse or keyboard: open/create projects, import media, build and edit sequences (insert, overwrite, move, trim, razor, ripple delete), effects, transitions and keyframes, audio levels and fades, markers, transcripts, preview frames and contact sheets, and exports, through an in-app UXP plugin the bridge installs itself.",+ "author": "Adom Inc.",+ "license": "MIT",+ "category": "Video",+ "tags": [+ "premiere",+ "premiere-pro",+ "adobe",+ "video",+ "video-editing",+ "timeline",+ "audio",+ "export"+ ],+ "languages": [+ "Rust",+ "JavaScript"+ ],+ "docs": "https://wiki.adom.inc/adom/premiere-bridge",+ "homepage": "https://wiki.adom.inc/adom/premiere-bridge",+ "spawn": {+ "kind": "python",+ "entrypoint": "launcher.py",+ "port": 0,+ "persistent": true,+ "healthEndpoint": "/health",+ "stopMethod": "kill",+ "killImageName": "premiere-bridge"+ },+ "verbPrefixes": [+ "premiere_"+ ],+ "verbs": [+ "premiere_describe",+ "premiere_status",+ "premiere_readiness",+ "premiere_install_plugin",+ "premiere_plugin_info",+ "premiere_launch",+ "premiere_quit",+ "premiere_wait_for_plugin",+ "premiere_events",+ "premiere_read_file_base64",+ "premiere_preview_frame",+ "premiere_contact_sheet",+ "premiere_monitor_snapshot",+ "premiere_list_export_presets",+ "premiere_ping",+ "premiere_get_project",+ "premiere_open_project",+ "premiere_new_project",+ "premiere_save_project",+ "premiere_close_project",+ "premiere_list_project_items",+ "premiere_import_media",+ "premiere_create_bin",+ "premiere_move_item",+ "premiere_rename_item",+ "premiere_delete_item",+ "premiere_set_item_color",+ "premiere_set_source_in_out",+ "premiere_create_subclip",+ "premiere_get_metadata",+ "premiere_set_xmp_metadata",+ "premiere_transcribe",+ "premiere_get_transcript",+ "premiere_list_sequences",+ "premiere_set_active_sequence",+ "premiere_create_sequence",+ "premiere_duplicate_sequence",+ "premiere_delete_sequence",+ "premiere_nest_selection",+ "premiere_get_sequence",+ "premiere_get_sequence_settings",+ "premiere_set_sequence_settings",+ "premiere_set_playhead",+ "premiere_set_in_out",+ "premiere_insert_clip",+ "premiere_overwrite_clip",+ "premiere_append_clip",+ "premiere_move_clip",+ "premiere_trim_clip",+ "premiere_remove_clips",+ "premiere_remove_gap",+ "premiere_razor",+ "premiere_set_clip_enabled",+ "premiere_rename_clip",+ "premiere_duplicate_clip",+ "premiere_select_clips",+ "premiere_get_selection",+ "premiere_clear_selection",+ "premiere_scene_edit_detection",+ "premiere_set_speed",+ "premiere_add_tracks",+ "premiere_remove_track",+ "premiere_lock_track",+ "premiere_set_track_target",+ "premiere_set_track_mute",+ "premiere_rename_track",+ "premiere_list_effects",+ "premiere_get_clip_effects",+ "premiere_add_effect",+ "premiere_remove_effect",+ "premiere_set_param",+ "premiere_add_keyframes",+ "premiere_remove_keyframes",+ "premiere_set_transform",+ "premiere_add_transition",+ "premiere_remove_transition",+ "premiere_insert_mogrt",+ "premiere_set_clip_volume",+ "premiere_audio_fade",+ "premiere_audio_probe",+ "premiere_list_markers",+ "premiere_add_marker",+ "premiere_remove_marker",+ "premiere_export_frame",+ "premiere_export_frames",+ "premiere_export_sequence",+ "premiere_export_interchange",+ "premiere_source_monitor",+ "premiere_eval",+ "premiere_extendscript",+ "premiere_export_frame_qe"+ ],+ "statusVerb": "premiere_status",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/premiere-bridge/files/adom-bridge-premiere-manifest-macos.json",+ "timeouts": {+ "default": 60,+ "verbs": {+ "launch": 180,+ "wait_for_plugin": 600,+ "install_plugin": 180,+ "open_project": 180,+ "import_media": 300,+ "export_frames": 300,+ "contact_sheet": 300,+ "eval": 600,+ "quit": 90+ },+ "prefixes": [+ {+ "prefix": "export_",+ "seconds": 300+ }+ ]+ },+ "detect": {+ "hostApp": "Adobe Premiere Pro",+ "paths": {+ "macos": [+ "/Applications/Adobe Premiere Pro 2027/Adobe Premiere Pro 2027.app",+ "/Applications/Adobe Premiere Pro 2026/Adobe Premiere Pro 2026.app",+ "/Applications/Adobe Premiere Pro 2025/Adobe Premiere Pro 2025.app"+ ],+ "windows": [+ "%ProgramFiles%\\Adobe\\Adobe Premiere Pro *\\Adobe Premiere Pro.exe"+ ]+ },+ "hostAppOptionalVerbs": [+ "describe",+ "status",+ "readiness",+ "plugin_info",+ "install_plugin",+ "launch",+ "wait_for_plugin",+ "events"+ ]+ },+ "prewarm": {+ "assets": []+ },+ "platforms": {+ "macos": {+ "supported": true,+ "reason": "Native Apple Silicon binary, tested against Premiere Pro 2026 (26.5). Premiere is driven through the Adom UXP plugin (+ optional CEP helper)."+ },+ "windows": {+ "supported": false,+ "reason": "The plugin and server are cross-platform, but no Windows binary has been built or tested yet."+ },+ "linux": {+ "supported": false,+ "reason": "Adobe doesn't ship Premiere Pro for Linux."+ }+ }+}
dev-skills/premiere-bridge-dev/SKILL.mdadded+58@@ -0,0 +1,58 @@+---+name: premiere-bridge-dev+description: Maintainer guide for the Premiere bridge source (Rust server + UXP plugin + CEP helper) — architecture, the verb table, the dev loop on a real Mac, and every premierepro API behavior measured on Premiere 26.5 (time bases, selection lifetime, item types, exporter quirks). Read before changing any premiere_* verb.+user-invocable: false+---++# Premiere bridge — developer guide++## THE RULE: the AI reads verb OUTPUT, not this file+Every verb response gets `_hint` / `_next` / `pitfalls` stamped from `src/catalog.rs`. When you add or change a verb, update its catalog row in the same commit. Failure hints come from `error_hint()` keyed by `errorCode`, so throw `fail(code, message)` in the plugin with a stable code.++## Layout+| Path | What |+|---|---|+| `src/main.rs` | axum server: `/health`, `POST /command` (JSON-only, 415/405 guards, no ACAO), `/ws` (plugin sockets, token in query). Routes verbs by runtime. |+| `src/catalog.rs` | The verb table: runtime (Local/Uxp/Cep/PreferCep), timeout, summary, args, hint, next, pitfalls. Drives `premiere_describe` and decoration. |+| `src/host.rs` | Local verbs: status/readiness/launch/quit, preview_frame + contact_sheet (inline base64, waits for async file writes), export-preset discovery, monitor_snapshot. |+| `src/plugin_pkg.rs` | Embeds `plugin/` with `include_str!`, builds the `.ccx`, installs via UPIA; copies the CEP helper + sets PlayerDebugMode. |+| `src/ab.rs` | Minimal HTTP client for Adom Bridge's direct API (`ADOM_DIRECT_API_URL` or `~/.adom/direct-api-port`). |+| `plugin/uxp/index.js` | Every UXP verb (`handlers`), helpers, connection loop. |+| `plugin/cep/` | Invisible CEP extension + `host.jsx` (ES3) for QE razor, speed, tracks, raw ExtendScript. |++Protocol (both runtimes): plugin → `{type:"hello", runtime:"uxp"|"cep", …}`; bridge → `{type:"call", id, verb, args}`; plugin → `{type:"result", id, ok, result|error, errorCode}`; plugin → `{type:"event", kind}` (export progress, project opened). The bridge pings every 15 s.++The plugin finds the bridge by reading `~/.adom/premiere-bridge/connection.json` (port + token) on every reconnect attempt, with a copy baked into the `.ccx` as a fallback.++## Dev loop (needs the desktop connected over adom-bridge)+```bash+scripts/dev-sync.sh # tar → send_files → cargo build --release on the Mac → restart test server :47911+scripts/pb.sh premiere_install_plugin '{"force":true}' # reinstall plugin; Premiere hot-reloads it (no restart)+scripts/pb.sh premiere_status+scripts/seq.sh # compact timeline dump+scripts/pb.sh premiere_eval '{"code":"return Object.keys(ppro.Constants)"}' # probe the live API+```+Test edits in a **scratch project** (`premiere_new_project` under ~/adom-dev), never in the user's project; close it with `close_project {save:false}` after checking `get_project.path`.++## Measured API facts (Premiere 26.5.2): don't relearn these+- **Actions must be built inside `project.lockedAccess`, synchronously.** Do all async reads first, then `tx(p, label, () => [actions])`.+- **`TrackItemSelection` is only valid inside `createEmptySelection`'s callback.** Using it later throws "The script object is no longer valid". Use `withSelection(clips, sel => …)`.+- **`ppro.Constants.ProjectItemType` doesn't exist.** Use `ppro.ProjectItem.TYPE_*` (CLIP=1, BIN=2, ROOT=3, FILE=4).+- **`TickTime.timeToTimecode` / `timecodeToTime` don't exist.** The plugin computes timecode itself (drop-frame at 29.97/59.94).+- `Constants.MediaType` values are GUID strings; `TrackItemType.CLIP` = 1; `TransitionPosition` START=1, END=2.+- **`createSequenceFromMedia(name, items)`** wants plain ProjectItems. Passing `undefined` as the bin throws "Illegal Parameter type", so omit the argument.+- **Trim semantics:** `createSetStartAction` and `createSetInPointAction` are both head trims that preserve (start − in). `createSetEndAction` / `createSetOutPointAction` are tail trims. `createMoveAction(delta)` shifts. A cloned item (`createCloneTrackItemAction`) is offset like a move, which breaks sync. That's why razor is implemented as tail-trim + temporary source in/out + overwrite.+- **Keyframe positions are in the clip's media time** (timeline − clip start + in point). The bridge converts from sequence time (`kfTime`). Enabling time-varying adds a key at the clip's in point.+- **Volume:** `Volume > Level` is linear; 0 dB = 0.17783 (`10^((dB-15)/20)`).+- **Motion > Position** is normalized `[0..1, 0..1]`.+- **`Exporter.exportSequenceFrame`** resolves before the file is fully written (the bridge waits for a stable size). It does NOT evaluate keyframes between two keys. Pass the filename WITH its extension; no doubled extension on 26.5.+- `EncoderManager.exportSequence(…, IMMEDIATELY, …)` returns after the render, with no encoder events.+- A UPIA install of the `.ccx` hot-loads into a running Premiere, and the plugin's JS runs without the panel being opened.+- UPIA rejects a manifest with `host` as an array and no `icons` ("uxp manifest file is invalid", status −267). Keep `host` an object and ship icons.+- CEP helper needs `PlayerDebugMode=1` (CSXS 9–13) and only loads at Premiere start.++## Adding a verb+1. Add the handler in `plugin/uxp/index.js` (or `ADOM_VERBS` in `host.jsx` for CEP).+2. Add its row to `VERBS` in `src/catalog.rs` with a real hint/next/pitfalls.+3. Re-generate `bridge.json` `verbs` (see the publish skill), rebuild, reinstall the plugin, and test on a scratch project.+4. Bump `PLUGIN_VERSION` / `BUNDLED_VERSION` and the manifest version together when the plugin changes.
dev-skills/premiere-bridge-publish/SKILL.mdadded+44@@ -0,0 +1,44 @@+---+name: premiere-bridge-publish+description: Release checklist for the Premiere bridge — version bump, macOS release zip (binary + bridge.json), manifest on the wiki page, skills pkg publish, discovery triggers, and verifying the install from Adom Bridge.+user-invocable: false+---++# Publishing the Premiere bridge++Wiki page: `adom/premiere-bridge`. Manifest: `adom-bridge-premiere-manifest-macos.json` on the page's `/files`. Runtime: a Release asset. Skills: the pkg.++## 1. Bump versions together+`Cargo.toml` `version`, `bridge.json` `version`, `src/plugin_pkg.rs` `BUNDLED_VERSION`, `plugin/uxp/manifest.json` + `index.js` `PLUGIN_VERSION`, `plugin/cep/CSXS/manifest.xml`, `package.json`.+Regenerate the verb list: `python3 scripts/sync-verbs.py`.++## 2. Build the runtime zip (on the Mac)+```bash+scripts/build-release.sh # builds, zips premiere-bridge + bridge.json at the ROOT, prints sha256 + size+unzip -l dist/adom-bridge-premiere-macos-v<ver>.zip # 2 files, ~1.5 MB. No media, no plugin/ dir (it's embedded)+```++## 3. Release + manifest+```bash+adom-wiki release create adom/premiere-bridge <ver> --title "Premiere bridge <ver>" --changelog "…"+adom-wiki release upload adom/premiere-bridge <ver> dist/adom-bridge-premiere-macos-v<ver>.zip --platform darwin-arm64+# manifest url → https://wiki.adom.inc/download/adom/premiere-bridge/<ver>/adom-bridge-premiere-macos-v<ver>.zip+adom-wiki repo push adom/premiere-bridge dist/adom-bridge-premiere-manifest-macos.json # (or the page files API)+```+Never `repo rm` an old zip that a live manifest still points to.++## 4. Skills pkg (text only)+```bash+adom-wiki pkg pack && tar tzf *.tgz # README, SKILL.md, package.json, install/uninstall, 3× skills/*/SKILL.md — nothing else+adom-wiki pkg publish --org adom+```+`dev-skills/` and `scripts/` must NOT be in the tarball (they're not in `files[]`).++## 5. Discovery+```bash+adom-wiki discover triggers adom/premiere-bridge --add "<phrases from package.json>"+adom-wiki discover preview --json "edit my video in premiere" # must be top-3; space calls ~2 s apart+```++## 6. Verify+`adom-bridge bridge_install {manifestUrl}` → `bridge_list` shows `premiere` at the new version → `adom-bridge premiere_status` through Adom Bridge (not the dev server) → `adom-bridge sync_skills` in a container pulls the skills.
install.shadded+15@@ -0,0 +1,15 @@+#!/usr/bin/env bash+# Installs the Premiere bridge USER skills into every agent skills home (Claude + Codex).+set -euo pipefail+HERE="$(cd "$(dirname "$0")" && pwd)"+install_skill() { # <dest-name> <src-file>+ for home in "$HOME/.claude/skills" "$HOME/.codex/skills"; do+ mkdir -p "$home/$1"+ cp "$HERE/$2" "$home/$1/SKILL.md"+ done+}+install_skill premiere-bridge-pack SKILL.md+install_skill premiere-bridge skills/premiere-bridge/SKILL.md+install_skill premiere-editing skills/premiere-editing/SKILL.md+install_skill premiere-preview-export skills/premiere-preview-export/SKILL.md+echo "premiere-bridge skills installed (Claude + Codex)"
launcher.pyadded+41@@ -0,0 +1,41 @@+"""Entrypoint Adom Bridge spawns (spawn.kind=python).++Adom Bridge's unzip doesn't keep Unix file modes, so the Rust binary arrives without its+executable bit. Copy it to ~/.adom/premiere-bridge/bin/ (never write beside this file:+on macOS that can break an app-bundle seal), mark it executable, and exec it in place so+the PID Adom Bridge supervises — and the --port argument it appended — carry straight+through to the real server.+"""+import hashlib+import os+import shutil+import stat+import sys++HERE = os.path.dirname(os.path.abspath(__file__))+SRC = os.path.join(HERE, "premiere-bridge.exe" if os.name == "nt" else "premiere-bridge")+++def main():+ with open(SRC, "rb") as f:+ digest = hashlib.sha256(f.read()).hexdigest()[:12]+ bindir = os.path.join(os.path.expanduser("~"), ".adom", "premiere-bridge", "bin")+ os.makedirs(bindir, exist_ok=True)+ dst = os.path.join(bindir, f"premiere-bridge-{digest}" + (".exe" if os.name == "nt" else ""))+ if not os.path.exists(dst):+ tmp = dst + ".tmp"+ shutil.copyfile(SRC, tmp)+ os.chmod(tmp, os.stat(tmp).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)+ os.replace(tmp, dst)+ # Drop older copies.+ for name in os.listdir(bindir):+ if name.startswith("premiere-bridge-") and os.path.join(bindir, name) != dst:+ try:+ os.remove(os.path.join(bindir, name))+ except OSError:+ pass+ os.execv(dst, [dst] + sys.argv[1:])+++if __name__ == "__main__":+ main()
package.jsonadded+69@@ -0,0 +1,69 @@+{+ "slug": "premiere-bridge",+ "name": "premiere-bridge",+ "version": "0.1.0",+ "type": "app",+ "description": "This package installs the Premiere bridge's SKILLS into your container so your AI knows how to drive Adobe Premiere Pro; Adom Bridge loads the bridge runtime itself from the release zip.",+ "tags": ["premiere", "premiere-pro", "adobe", "video", "video-editing", "timeline", "bridge", "adom-bridge", "desktop-app"],+ "dependencies": { "adom/adom-bridge": "^2.0.3" },+ "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" },+ "files": [+ "README.md",+ "SKILL.md",+ "package.json",+ "install.sh",+ "uninstall.sh",+ "skills/premiere-bridge/SKILL.md",+ "skills/premiere-editing/SKILL.md",+ "skills/premiere-preview-export/SKILL.md"+ ],+ "discovery_triggers": [+ "edit my video in premiere",+ "open premiere",+ "open my premiere project",+ "launch premiere pro",+ "adobe premiere",+ "premiere pro timeline",+ "edit the timeline in premiere",+ "add a clip to the timeline",+ "add clips to my video",+ "import footage into premiere",+ "make a rough cut",+ "cut this part out of the video",+ "trim the clip",+ "razor the clip",+ "ripple delete in premiere",+ "close the gaps in the timeline",+ "add a transition in premiere",+ "add a cross dissolve",+ "fade to black",+ "add an effect to the clip",+ "blur the clip in premiere",+ "zoom in on the clip",+ "lower the volume of the clip",+ "fade out the music",+ "fade in the audio",+ "duck the music under the voiceover",+ "edit the audio in premiere",+ "add a marker in premiere",+ "add a title in premiere",+ "show me a frame from my video",+ "preview my premiere edit",+ "storyboard of my cut",+ "export the video from premiere",+ "render my premiere sequence",+ "export for youtube from premiere",+ "transcribe my clip in premiere",+ "control premiere with ai",+ "premiere_get_sequence",+ "premiere_razor",+ "premiere_preview_frame"+ ],+ "discovery_pitch": "Use the Premiere bridge when the user wants the AI to EDIT VIDEO in Adobe Premiere Pro on their desktop (import, cut, trim, effects, audio, previews, export) without touching their mouse. Check `adom-bridge bridge_list` for a `premiere` bridge; if missing, `adom-bridge bridge_install {\"manifestUrl\":\"https://wiki.adom.inc/api/v1/pages/premiere-bridge/files/adom-bridge-premiere-manifest-macos.json\"}`. If this container lacks the skill, `adom-wiki pkg install adom/premiere-bridge`. Then start with `adom-bridge premiere_status`. NOT for generating video from scratch in the cloud (use adom-video-post / Movie Maker for that).",+ "sample_prompts": [+ { "label": "See the cut", "prompt": "Show me a storyboard of my current Premiere sequence." },+ { "label": "Tighten it", "prompt": "In Premiere, cut out 12.0 to 15.5 seconds and close the gap." },+ { "label": "Audio pass", "prompt": "Lower the music on A2 to -18 dB and fade it out over the last 2 seconds." },+ { "label": "Export", "prompt": "Export my Premiere sequence as a YouTube 1080p MP4 and bring it here." }+ ]+}
plugin/cep/CSXS/manifest.xmladded+48@@ -0,0 +1,48 @@+<?xml version="1.0" encoding="UTF-8"?>+<!-- Adom Bridge CEP helper: an INVISIBLE extension that starts with Premiere and covers+ what UXP can't (true razor, clip speed, adding tracks, raw ExtendScript), and keeps the+ bridge reachable before the UXP panel has ever been opened. -->+<ExtensionManifest Version="7.0" ExtensionBundleId="inc.adom.premiere.cep" ExtensionBundleVersion="0.1.0" ExtensionBundleName="Adom Bridge Helper">+ <ExtensionList>+ <Extension Id="inc.adom.premiere.cep.helper" Version="0.1.0" />+ </ExtensionList>+ <ExecutionEnvironment>+ <HostList>+ <Host Name="PPRO" Version="[15.0,99.9]" />+ </HostList>+ <LocaleList>+ <Locale Code="All" />+ </LocaleList>+ <RequiredRuntimeList>+ <RequiredRuntime Name="CSXS" Version="9.0" />+ </RequiredRuntimeList>+ </ExecutionEnvironment>+ <DispatchInfoList>+ <Extension Id="inc.adom.premiere.cep.helper">+ <DispatchInfo>+ <Resources>+ <MainPath>./index.html</MainPath>+ <ScriptPath>./host.jsx</ScriptPath>+ <CEFCommandLine>+ <Parameter>--enable-nodejs</Parameter>+ <Parameter>--mixed-context</Parameter>+ </CEFCommandLine>+ </Resources>+ <Lifecycle>+ <AutoVisible>false</AutoVisible>+ <StartOn>+ <Event>com.adobe.csxs.events.ApplicationActivate</Event>+ <Event>applicationActivate</Event>+ <Event>com.adobe.csxs.events.ApplicationInitialized</Event>+ </StartOn>+ </Lifecycle>+ <UI>+ <Type>Custom</Type>+ <Geometry>+ <Size><Height>1</Height><Width>1</Width></Size>+ </Geometry>+ </UI>+ </DispatchInfo>+ </Extension>+ </DispatchInfoList>+</ExtensionManifest>
plugin/cep/host.jsxadded+173@@ -0,0 +1,173 @@+// Adom Bridge — ExtendScript side of the CEP helper (ES3: no JSON, no let/const).+// adom_dispatch(jsonString) -> JSON string {ok, result|error, errorCode}++function adom_json(v) {+ var t = typeof v;+ if (v === null || v === undefined) return "null";+ if (t === "number") return isFinite(v) ? String(v) : "null";+ if (t === "boolean") return v ? "true" : "false";+ if (t === "string") {+ return '"' + v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"';+ }+ if (v instanceof Array) {+ var a = [];+ for (var i = 0; i < v.length; i++) a.push(adom_json(v[i]));+ return "[" + a.join(",") + "]";+ }+ if (t === "object") {+ var parts = [];+ for (var k in v) {+ if (v.hasOwnProperty(k) && typeof v[k] !== "function") parts.push(adom_json(String(k)) + ":" + adom_json(v[k]));+ }+ return "{" + parts.join(",") + "}";+ }+ return adom_json(String(v));+}++function adom_hello() {+ var proj = app.project ? app.project.name : null;+ var seq = app.project && app.project.activeSequence ? app.project.activeSequence.name : null;+ return adom_json({ premiereVersion: app.version, build: app.build, project: proj, sequence: seq });+}++function adom_fail(code, msg) {+ var e = new Error(msg);+ e.adomCode = code;+ return e;+}++function adom_seq() {+ var seq = app.project.activeSequence;+ if (!seq) throw adom_fail("no_sequence", "No active sequence");+ app.enableQE();+ var qseq = qe.project.getActiveSequence();+ if (!qseq) throw adom_fail("no_sequence", "QE has no active sequence");+ return { seq: seq, qseq: qseq };+}++// "V1" -> {video:true, index:0}+function adom_track(ref) {+ var m = /^([VvAa])(\d+)$/.exec(String(ref));+ if (!m) throw adom_fail("bad_track", "Bad track '" + ref + "' (use V1, A2 …)");+ return { video: m[1].toUpperCase() === "V", index: parseInt(m[2], 10) - 1 };+}++function adom_timecode(seq, seconds) {+ var t = new Time();+ t.seconds = Number(seconds);+ var s = seq.getSettings();+ return t.getFormatted(s.videoFrameRate, s.videoDisplayFormat);+}++// The QE item at (track, clipIndex) where clipIndex counts real clips in time order —+// the same numbering the UXP side uses ("V1:0"). QE lists gaps as "Empty" items.+function adom_qeClip(qtrack, clipIndex) {+ var n = qtrack.numItems;+ var k = 0;+ for (var i = 0; i < n; i++) {+ var it = qtrack.getItemAt(i);+ if (!it || it.type === "Empty") continue;+ if (k === clipIndex) return it;+ k++;+ }+ throw adom_fail("clip_not_found", "No clip index " + clipIndex + " on that track");+}++var ADOM_VERBS = {+ ping: function () {+ return eval("(" + adom_hello() + ")");+ },++ razor: function (a) {+ var c = adom_seq();+ var tc = adom_timecode(c.seq, a.time);+ var cut = [];+ var i;+ var want = null;+ if (a.tracks) {+ want = {};+ for (i = 0; i < a.tracks.length; i++) want[String(a.tracks[i]).toUpperCase()] = true;+ }+ for (i = 0; i < c.qseq.numVideoTracks; i++) {+ if (want && !want["V" + (i + 1)]) continue;+ c.qseq.getVideoTrackAt(i).razor(tc);+ cut.push("V" + (i + 1));+ }+ for (i = 0; i < c.qseq.numAudioTracks; i++) {+ if (want && !want["A" + (i + 1)]) continue;+ c.qseq.getAudioTrackAt(i).razor(tc);+ cut.push("A" + (i + 1));+ }+ return { cutTracks: cut, timecode: tc, method: "qe-razor" };+ },++ set_speed: function (a) {+ var c = adom_seq();+ var m = /^([VvAa]\d+):(\d+)$/.exec(String(a.clip));+ if (!m) throw adom_fail("bad_clip", "clip must look like V1:0");+ var tr = adom_track(m[1]);+ var qtrack = tr.video ? c.qseq.getVideoTrackAt(tr.index) : c.qseq.getAudioTrackAt(tr.index);+ var item = adom_qeClip(qtrack, parseInt(m[2], 10));+ // setSpeed(speed, unused, reverse, ripple, maintainAudioPitch)+ item.setSpeed(Number(a.speed), "", !!a.reverse, !!a.ripple, a.maintainPitch !== false);+ return { clip: a.clip, speed: Number(a.speed), reverse: !!a.reverse, ripple: !!a.ripple };+ },++ add_tracks: function (a) {+ var c = adom_seq();+ var v = Number(a.video || 0);+ var au = Number(a.audio || 0);+ // addTracks(numVideo, insertAfterVideoIdx, numAudio, audioType(0 mono,1 stereo,2 5.1), insertAfterAudioIdx, numSubmix, submixType, insertAfterSubmix)+ c.qseq.addTracks(v, c.qseq.numVideoTracks, au, a.audioType === "mono" ? 0 : a.audioType === "5.1" ? 2 : 1, c.qseq.numAudioTracks, 0, 0, 0);+ return { videoTracks: c.qseq.numVideoTracks, audioTracks: c.qseq.numAudioTracks };+ },++ remove_track: function (a) {+ var c = adom_seq();+ var tr = adom_track(a.track);+ if (tr.video) c.qseq.removeVideoTrack(tr.index);+ else c.qseq.removeAudioTrack(tr.index);+ return { removed: a.track };+ },++ lock_track: function (a) {+ var c = adom_seq();+ var tr = adom_track(a.track);+ var t = tr.video ? c.seq.videoTracks[tr.index] : c.seq.audioTracks[tr.index];+ t.setLocked(a.locked !== false ? 1 : 0);+ return { track: a.track, locked: a.locked !== false };+ },++ set_track_target: function (a) {+ var c = adom_seq();+ var tr = adom_track(a.track);+ var t = tr.video ? c.seq.videoTracks[tr.index] : c.seq.audioTracks[tr.index];+ t.setTargeted(a.targeted !== false, true);+ return { track: a.track, targeted: a.targeted !== false };+ },++ export_frame_qe: function (a) {+ var c = adom_seq();+ var tc = adom_timecode(c.seq, a.time);+ c.qseq.exportFramePNG(tc, a.path);+ return { path: a.path, timecode: tc };+ },++ extendscript: function (a) {+ // a.code is evaluated as a function body; `return` a value. Has app, qe (after app.enableQE()).+ app.enableQE();+ var f = new Function("args", a.code);+ return { value: f(a.args || {}) };+ },+};++function adom_dispatch(payload) {+ try {+ var req = eval("(" + payload + ")");+ var fn = ADOM_VERBS[req.verb];+ if (!fn) return adom_json({ ok: false, errorCode: "not_in_cep", error: "CEP helper has no verb '" + req.verb + "'" });+ return adom_json({ ok: true, result: fn(req.args || {}) });+ } catch (e) {+ return adom_json({ ok: false, errorCode: e.adomCode || "extendscript_error", error: String(e.message || e), line: e.line });+ }+}
plugin/cep/index.htmladded+7@@ -0,0 +1,7 @@+<!DOCTYPE html>+<html>+<head><meta charset="utf-8"><title>Adom Bridge Helper</title></head>+<body>+ <script src="main.js"></script>+</body>+</html>
plugin/cep/main.jsadded+90@@ -0,0 +1,90 @@+// Adom Bridge CEP helper — invisible; dials the Rust bridge's WebSocket and runs+// ExtendScript (host.jsx) for the verbs UXP can't do. Same protocol as the UXP plugin.+(function () {+ "use strict";+ var HELPER_VERSION = "0.1.0";+ var nodeRequire = typeof require !== "undefined" ? require : window.cep_node && window.cep_node.require;+ var fs = nodeRequire("fs");+ var os = nodeRequire("os");+ var path = nodeRequire("path");++ var ws = null;+ var backoff = 1000;++ function evalScript(script) {+ return new Promise(function (resolve) {+ window.__adobe_cep__.evalScript(script, function (res) {+ resolve(res);+ });+ });+ }++ function readConnection() {+ try {+ var p = path.join(os.homedir(), ".adom", "premiere-bridge", "connection.json");+ var j = JSON.parse(fs.readFileSync(p, "utf8"));+ return j && j.port && j.token ? j : null;+ } catch (e) {+ return null;+ }+ }++ function send(obj) {+ if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));+ }++ function connect() {+ if (ws && ws.readyState <= 1) return;+ var conn = readConnection();+ if (!conn) return retry();+ var sock = new WebSocket("ws://127.0.0.1:" + conn.port + "/ws?token=" + encodeURIComponent(conn.token));+ ws = sock;+ sock.onopen = function () {+ backoff = 1000;+ evalScript("adom_hello()").then(function (res) {+ var info = {};+ try {+ info = JSON.parse(res);+ } catch (e) {}+ info.type = "hello";+ info.runtime = "cep";+ info.helperVersion = HELPER_VERSION;+ info.protocol = 1;+ send(info);+ });+ };+ sock.onmessage = function (ev) {+ var msg;+ try {+ msg = JSON.parse(ev.data);+ } catch (e) {+ return;+ }+ if (msg.type === "ping") return send({ type: "pong" });+ if (msg.type !== "call") return;+ var payload = JSON.stringify(JSON.stringify({ verb: msg.verb, args: msg.args || {} }));+ evalScript("adom_dispatch(" + payload + ")").then(function (res) {+ var out;+ try {+ out = JSON.parse(res);+ } catch (e) {+ out = { ok: false, error: "ExtendScript returned non-JSON: " + String(res).slice(0, 500), errorCode: "extendscript_error" };+ }+ out.type = "result";+ out.id = msg.id;+ send(out);+ });+ };+ sock.onclose = function () {+ if (ws === sock) ws = null;+ retry();+ };+ }++ function retry() {+ setTimeout(connect, backoff);+ backoff = Math.min(backoff * 2, 5000);+ }++ connect();+})();
plugin/uxp/icons/[email protected]added⋯ 1 unchanged line ⋯
plugin/uxp/icons/[email protected]added⋯ 1 unchanged line ⋯
plugin/uxp/icons/[email protected]added⋯ 1 unchanged line ⋯
plugin/uxp/icons/[email protected]added⋯ 1 unchanged line ⋯
plugin/uxp/index.htmladded+21@@ -0,0 +1,21 @@+<!DOCTYPE html>+<html>+<head>+ <meta charset="utf-8">+ <style>+ body { font-family: sans-serif; font-size: 12px; color: #ddd; background: #232323; margin: 10px; }+ .row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }+ .led { width: 10px; height: 10px; border-radius: 50%; background: #888; }+ .led.ok { background: #3fb950; }+ .led.wait { background: #d29922; }+ #log { color: #999; font-family: monospace; font-size: 11px; white-space: pre-wrap; max-height: 300px; overflow: auto; }+ </style>+</head>+<body>+ <div class="row"><div id="led" class="led wait"></div><b>Adom Bridge</b></div>+ <div id="status">Connecting…</div>+ <div id="last"></div>+ <div id="log"></div>+ <script src="index.js"></script>+</body>+</html>
plugin/uxp/index.jsadded+1700@@ -0,0 +1,1700 @@+// Adom Bridge — UXP plugin for Adobe Premiere Pro.+//+// Dials OUT to the Rust bridge's loopback WebSocket (UXP can't host a server),+// receives {type:"call", id, verb, args}, runs the verb against the `premierepro`+// API and answers {type:"result", id, ok, result|error}.+//+// Rules this file lives by:+// * Since Premiere 26.3 every create*Action must be built INSIDE project.lockedAccess,+// and lockedAccess callbacks are synchronous — so do every async read first, then+// build actions in one synchronous pass (see tx()).+// * Anything we hand back must be JSON-safe: times go out as {seconds, ticks, timecode}.++const ppro = require("premierepro");+const fs = require("fs");+const os = require("os");++const PLUGIN_VERSION = "0.1.0";+const PROTOCOL = 1;+const C = ppro.Constants;+const TICKS_PER_SECOND = 254016000000;++// ───────────────────────────── UI ─────────────────────────────++const ui = {+ led: document.getElementById("led"),+ status: document.getElementById("status"),+ last: document.getElementById("last"),+ log: document.getElementById("log"),+};+const logLines = [];+function log(msg) {+ const line = `${new Date().toLocaleTimeString()} ${msg}`;+ logLines.push(line);+ if (logLines.length > 40) logLines.shift();+ if (ui.log) ui.log.textContent = logLines.join("\n");+ console.log("[adom]", msg);+}+function setStatus(state, text) {+ if (ui.led) ui.led.className = "led " + (state === "ok" ? "ok" : "wait");+ if (ui.status) ui.status.textContent = text;+}++// ───────────────────────────── connection ─────────────────────────────++let ws = null;+let backoff = 1000;+let connecting = false;++function connectionPaths() {+ const home = (os.homedir && os.homedir()) || "";+ const sep = home.includes("\\") ? "\\" : "/";+ const p = [home, ".adom", "premiere-bridge", "connection.json"].join(sep);+ return [p, "file:" + p, "plugin:/connection.json"];+}++async function readConnection() {+ for (const p of connectionPaths()) {+ try {+ const txt = await fs.readFile(p, { encoding: "utf-8" });+ const j = JSON.parse(txt);+ if (j && j.port && j.token) return j;+ } catch (_) {+ /* try the next location */+ }+ }+ return null;+}++async function connect() {+ if (connecting || (ws && ws.readyState <= 1)) return;+ connecting = true;+ try {+ const conn = await readConnection();+ if (!conn) {+ setStatus("wait", "Waiting for Adom Bridge (no connection file yet)…");+ return scheduleReconnect();+ }+ const url = `ws://127.0.0.1:${conn.port}/ws?token=${encodeURIComponent(conn.token)}`;+ setStatus("wait", `Connecting to bridge on port ${conn.port}…`);+ const sock = new WebSocket(url);+ ws = sock;+ sock.onopen = async () => {+ backoff = 1000;+ setStatus("ok", `Connected (port ${conn.port})`);+ log("connected");+ send({ type: "hello", runtime: "uxp", ...(await helloInfo()) });+ };+ sock.onmessage = (ev) => onMessage(ev.data);+ sock.onclose = () => {+ if (ws === sock) ws = null;+ setStatus("wait", "Disconnected — retrying…");+ scheduleReconnect();+ };+ sock.onerror = () => {+ /* onclose follows */+ };+ } catch (e) {+ log("connect error: " + e);+ scheduleReconnect();+ } finally {+ connecting = false;+ }+}++function scheduleReconnect() {+ setTimeout(connect, backoff);+ backoff = Math.min(backoff * 2, 5000);+}++function send(obj) {+ if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj, jsonReplacer));+}++async function helloInfo() {+ let project = null;+ let sequence = null;+ try {+ const p = await ppro.Project.getActiveProject();+ if (p) {+ project = p.name;+ const s = await p.getActiveSequence();+ if (s) sequence = s.name;+ }+ } catch (_) {}+ let premiereVersion = "?";+ try {+ const host = require("uxp").host;+ premiereVersion = host.version;+ } catch (_) {}+ return { pluginVersion: PLUGIN_VERSION, protocol: PROTOCOL, premiereVersion, project, sequence, os: os.platform && os.platform() };+}++async function onMessage(data) {+ let msg;+ try {+ msg = JSON.parse(data);+ } catch (_) {+ return;+ }+ if (msg.type === "ping") return send({ type: "pong" });+ if (msg.type !== "call") return;+ const { id, verb, args } = msg;+ const fn = handlers[verb];+ if (ui.last) ui.last.textContent = `Last: ${verb}`;+ if (!fn) {+ return send({ type: "result", id, ok: false, errorCode: "not_in_uxp", error: `UXP plugin has no verb '${verb}'` });+ }+ const warnings = [];+ try {+ const result = await fn(args || {}, { warn: (w) => warnings.push(w) });+ send({ type: "result", id, ok: true, result: result === undefined ? null : result, warnings: warnings.length ? warnings : undefined });+ } catch (e) {+ log(`${verb} failed: ${e && e.message ? e.message : e}`);+ send({+ type: "result",+ id,+ ok: false,+ error: String(e && e.message ? e.message : e),+ errorCode: (e && e.code) || "premiere_error",+ stack: e && e.stack ? String(e.stack).split("\n").slice(0, 6).join("\n") : undefined,+ });+ }+}++function fail(code, message) {+ const e = new Error(message);+ e.code = code;+ return e;+}++function jsonReplacer(_k, v) {+ if (v && typeof v === "object" && typeof v.ticks === "string" && typeof v.seconds === "number" && !("timecode" in v)) {+ return { seconds: v.seconds, ticks: v.ticks };+ }+ if (typeof v === "bigint") return v.toString();+ return v;+}++// ───────────────────────────── helpers: project / sequence ─────────────────────────────++async function project() {+ const p = await ppro.Project.getActiveProject();+ if (!p) throw fail("no_project", "No project is open in Premiere. Use premiere_open_project or premiere_new_project.");+ return p;+}++async function findSequence(p, ref) {+ if (ref === undefined || ref === null || ref === "") {+ const s = await p.getActiveSequence();+ if (!s) throw fail("no_sequence", "No active sequence. Use premiere_create_sequence or premiere_set_active_sequence.");+ return s;+ }+ const seqs = await p.getSequences();+ for (const s of seqs) {+ if (s.guid.toString() === String(ref) || s.name === String(ref)) return s;+ }+ throw fail("sequence_not_found", `No sequence matching '${ref}'. Sequences: ${seqs.map((s) => s.name).join(", ")}`);+}++async function seqCtx(args) {+ const p = await project();+ const seq = await findSequence(p, args.sequence);+ return { p, seq };+}++async function frameRate(seq) {+ const settings = await seq.getSettings();+ return settings.getVideoFrameRate();+}++async function timeDisplay(seq) {+ return await seq.getSequenceVideoTimeDisplayFormat();+}++/** Seconds (number), timecode ("00:00:05:12"), "5s", "120f", {seconds}|{ticks}|{frames}. */+async function toTick(v, seq) {+ if (v === undefined || v === null) return undefined;+ if (typeof v === "object" && v.ticks !== undefined) return ppro.TickTime.createWithTicks(String(v.ticks));+ if (typeof v === "object" && v.seconds !== undefined) return ppro.TickTime.createWithSeconds(Number(v.seconds));+ if (typeof v === "object" && v.frames !== undefined) {+ return ppro.TickTime.createWithFrameAndFrameRate(Number(v.frames), await frameRate(seq));+ }+ if (typeof v === "number") return ppro.TickTime.createWithSeconds(v);+ const s = String(v).trim();+ if (/^-?\d+(\.\d+)?s?$/.test(s)) return ppro.TickTime.createWithSeconds(parseFloat(s));+ if (/^\d+f$/.test(s)) return ppro.TickTime.createWithFrameAndFrameRate(parseInt(s, 10), await frameRate(seq));+ if (/^\d+[:;]\d+[:;]\d+[:;]\d+$/.test(s)) {+ const fr = await frameRate(seq);+ return ppro.TickTime.createWithSeconds(timecodeToSeconds(s, fr.value));+ }+ throw fail("bad_time", `Can't parse time '${v}'. Use seconds (12.5), a timecode ("00:00:12:15"), "300f", or {ticks}.`);+}++async function timeOut(t, seq) {+ if (!t) return null;+ const o = { seconds: round(t.seconds), ticks: t.ticks };+ if (seq) {+ try {+ const fr = await frameRate(seq);+ o.timecode = secondsToTimecode(t.seconds, fr.value);+ o.frame = Math.round((t.ticksNumber || Number(t.ticks)) / fr.ticksPerFrame);+ } catch (_) {}+ }+ return o;+}++// 29.97/59.94 use drop-frame (";" separator) like Premiere's default display.+function isDropFrame(fps) {+ return Math.abs(fps - 29.97) < 0.01 || Math.abs(fps - 59.94) < 0.01;+}++function secondsToTimecode(seconds, fps) {+ const nominal = Math.round(fps);+ let frames = Math.round(seconds * fps);+ const drop = isDropFrame(fps);+ if (drop) {+ const d = nominal === 60 ? 4 : 2; // frames dropped per minute+ const perMin = nominal * 60 - d;+ const per10 = perMin * 10 + d;+ const tens = Math.floor(frames / per10);+ const rem = frames % per10;+ frames += d * 9 * tens + (rem > d ? d * Math.floor((rem - d) / perMin) : 0);+ }+ const ff = frames % nominal;+ const totalSec = Math.floor(frames / nominal);+ const pad = (n) => String(n).padStart(2, "0");+ const sep = drop ? ";" : ":";+ return `${pad(Math.floor(totalSec / 3600))}:${pad(Math.floor(totalSec / 60) % 60)}:${pad(totalSec % 60)}${sep}${pad(ff)}`;+}++function timecodeToSeconds(tc, fps) {+ const parts = tc.split(/[:;]/).map((n) => parseInt(n, 10));+ const [hh, mm, ss, ff] = parts;+ const nominal = Math.round(fps);+ let frames = ((hh * 60 + mm) * 60 + ss) * nominal + ff;+ if (isDropFrame(fps) && /;/.test(tc)) {+ const d = nominal === 60 ? 4 : 2;+ const totalMin = hh * 60 + mm;+ frames -= d * (totalMin - Math.floor(totalMin / 10));+ }+ return frames / fps;+}++function round(n) {+ return Math.round(n * 1000) / 1000;+}++/** Run actions as ONE undoable step. `build` runs synchronously inside lockedAccess. */+function tx(p, label, build) {+ let ok = false;+ let err = null;+ p.lockedAccess(() => {+ try {+ ok = p.executeTransaction((ca) => {+ const actions = build();+ for (const a of [].concat(actions).filter(Boolean)) ca.addAction(a);+ }, label || "Adom Bridge");+ } catch (e) {+ err = e;+ }+ });+ if (err) throw err;+ if (ok === false) throw fail("transaction_failed", `Premiere rejected the edit '${label}'. Check the clip/time arguments (e.g. a trim past the media's length).`);+ return true;+}++// ───────────────────────────── helpers: project items ─────────────────────────────++// Item type numbers come from ppro.ProjectItem.TYPE_* (Constants.ProjectItemType isn't+// exposed at runtime on 26.x).+function typeName(t) {+ const PI = ppro.ProjectItem;+ const map = { [PI.TYPE_BIN]: "bin", [PI.TYPE_CLIP]: "clip", [PI.TYPE_FILE]: "file", [PI.TYPE_ROOT]: "root" };+ if (PI.TYPE_COMPOUND !== undefined) map[PI.TYPE_COMPOUND] = "compound";+ if (PI.TYPE_STYLE !== undefined) map[PI.TYPE_STYLE] = "style";+ return map[t] || String(t);+}++async function walkItems(folder, path, out, depth, maxDepth) {+ const items = await folder.getItems();+ for (const it of items) {+ const type = typeName(it.type);+ const rec = { item: it, name: it.name, type, bin: path, id: safe(() => it.getId()) };+ out.push(rec);+ if (type === "bin" && depth < maxDepth) {+ await walkItems(ppro.FolderItem.cast(it), path ? `${path}/${it.name}` : it.name, out, depth + 1, maxDepth);+ }+ }+ return out;+}++function safe(fn, dflt = null) {+ try {+ return fn();+ } catch (_) {+ return dflt;+ }+}++async function allItems(p) {+ return walkItems(await p.getRootItem(), "", [], 0, 32);+}++/** Item ref: id string, exact name, bin path "Footage/clip.mp4", or {path: mediaFilePath}. */+async function findItem(p, ref) {+ if (!ref) throw fail("bad_args", "item is required (project item id, name, 'Bin/name', or {mediaPath})");+ const recs = await allItems(p);+ const want = typeof ref === "object" ? ref : { key: String(ref) };+ for (const r of recs) {+ if (want.key !== undefined) {+ if (r.id === want.key || r.name === want.key || (r.bin ? `${r.bin}/${r.name}` : r.name) === want.key) return r.item;+ }+ }+ const mediaPath = want.mediaPath || want.path || (want.key && /[\\/]/.test(want.key) ? want.key : null);+ if (mediaPath) {+ for (const r of recs) {+ if (r.type !== "clip" && r.type !== "file") continue;+ const clip = ppro.ClipProjectItem.cast(r.item);+ const mp = clip ? await clip.getMediaFilePath().catch(() => "") : "";+ if (mp && normPath(mp) === normPath(mediaPath)) return r.item;+ }+ }+ throw fail("item_not_found", `No project item matching ${JSON.stringify(ref)}. Call premiere_list_project_items to see ids and names.`);+}++function normPath(s) {+ return String(s).replace(/\\/g, "/").toLowerCase();+}++async function describeItem(rec, withMedia) {+ const o = { id: rec.id, name: rec.name, type: rec.type, bin: rec.bin || "/" };+ if (withMedia && (rec.type === "clip" || rec.type === "file")) {+ const clip = ppro.ClipProjectItem.cast(rec.item);+ if (clip) {+ o.mediaPath = await clip.getMediaFilePath().catch(() => null);+ o.isSequence = await clip.isSequence().catch(() => false);+ o.offline = await clip.isOffline().catch(() => null);+ try {+ const media = await clip.getMedia();+ if (media) o.duration = round((await media.duration).seconds);+ } catch (_) {}+ }+ }+ return o;+}++async function binFor(p, ref) {+ if (!ref || ref === "/" ) return await p.getRootItem();+ const it = await findItem(p, ref);+ const f = ppro.FolderItem.cast(it);+ if (!f) throw fail("not_a_bin", `'${ref}' is not a bin`);+ return f;+}++// ───────────────────────────── helpers: tracks & clips ─────────────────────────────++/** "V1" / "A2" / {type:"video", index:0}. Track numbers in refs are 1-based like the UI. */+function parseTrack(ref) {+ if (ref && typeof ref === "object") {+ return { kind: String(ref.type || ref.kind || "video").startsWith("a") ? "audio" : "video", index: Number(ref.index || 0) };+ }+ const m = /^([VvAa])(\d+)$/.exec(String(ref || "V1").trim());+ if (!m) throw fail("bad_track", `Bad track '${ref}'. Use "V1", "V2", "A1"…`);+ return { kind: m[1].toUpperCase() === "V" ? "video" : "audio", index: parseInt(m[2], 10) - 1 };+}++function trackLabel(kind, index) {+ return (kind === "video" ? "V" : "A") + (index + 1);+}++async function getTrack(seq, kind, index) {+ const count = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();+ if (index < 0 || index >= count) throw fail("track_not_found", `${trackLabel(kind, index)} doesn't exist (sequence has ${count} ${kind} tracks). premiere_add_tracks can add more.`);+ return kind === "video" ? await seq.getVideoTrack(index) : await seq.getAudioTrack(index);+}++async function trackClips(track) {+ const items = track.getTrackItems(C.TrackItemType.CLIP, false) || [];+ const withStart = [];+ for (const it of items) withStart.push({ it, start: (await it.getStartTime()).ticksNumber });+ withStart.sort((a, b) => a.start - b.start);+ return withStart.map((x) => x.it);+}++/** Clip ref: "V1:0" (track:index, 0-based in time order), {track:"V1", index}, or {track:"V1", at: seconds}. */+async function findClip(seq, ref) {+ let trackRef, index, at;+ if (typeof ref === "string") {+ const m = /^([VvAa]\d+):(\d+)$/.exec(ref.trim());+ if (!m) throw fail("bad_clip", `Bad clip ref '${ref}'. Use "V1:0" (track:index) — premiere_get_sequence lists them.`);+ trackRef = m[1];+ index = parseInt(m[2], 10);+ } else if (ref && typeof ref === "object") {+ trackRef = ref.track;+ index = ref.index;+ at = ref.at;+ } else {+ throw fail("bad_clip", "clip is required, e.g. \"V1:0\"");+ }+ const { kind, index: tIdx } = parseTrack(trackRef);+ const track = await getTrack(seq, kind, tIdx);+ const clips = await trackClips(track);+ if (at !== undefined) {+ const t = (await toTick(at, seq)).ticksNumber;+ for (let i = 0; i < clips.length; i++) {+ const s = (await clips[i].getStartTime()).ticksNumber;+ const e = (await clips[i].getEndTime()).ticksNumber;+ if (t >= s && t < e) return { clip: clips[i], kind, trackIndex: tIdx, index: i, ref: `${trackLabel(kind, tIdx)}:${i}` };+ }+ throw fail("clip_not_found", `No clip on ${trackLabel(kind, tIdx)} at ${at}`);+ }+ if (index === undefined || index < 0 || index >= clips.length) {+ throw fail("clip_not_found", `${trackLabel(kind, tIdx)} has ${clips.length} clips; index ${index} is out of range. premiere_get_sequence lists them.`);+ }+ return { clip: clips[index], kind, trackIndex: tIdx, index, ref: `${trackLabel(kind, tIdx)}:${index}` };+}++async function describeClip(c, seq, kind, tIdx, i, detail) {+ const o = {+ ref: `${trackLabel(kind, tIdx)}:${i}`,+ name: await c.getName().catch(() => null),+ start: await timeOut(await c.getStartTime(), seq),+ end: await timeOut(await c.getEndTime(), seq),+ duration: round((await c.getDuration()).seconds),+ inPoint: round((await c.getInPoint()).seconds),+ outPoint: round((await c.getOutPoint()).seconds),+ disabled: await c.isDisabled().catch(() => false),+ selected: await c.getIsSelected().catch(() => false),+ };+ if (detail) {+ o.speed = await c.getSpeed().catch(() => null);+ o.isAdjustmentLayer = await c.isAdjustmentLayer().catch(() => false);+ try {+ const pi = await c.getProjectItem();+ if (pi) {+ o.projectItem = { id: safe(() => pi.getId()), name: pi.name };+ const clip = ppro.ClipProjectItem.cast(pi);+ if (clip) o.projectItem.mediaPath = await clip.getMediaFilePath().catch(() => null);+ }+ } catch (_) {}+ try {+ const chain = await c.getComponentChain();+ const names = [];+ for (let k = 0; k < chain.getComponentCount(); k++) names.push(await chain.getComponentAtIndex(k).getDisplayName());+ o.effects = names;+ } catch (_) {}+ }+ return o;+}++/** Clips on the other tracks that are linked to this one (same source, same start/end). */+async function linkedPartners(seq, target) {+ const pi = await target.clip.getProjectItem().catch(() => null);+ const id = pi && safe(() => pi.getId());+ const S = (await target.clip.getStartTime()).ticks;+ const E = (await target.clip.getEndTime()).ticks;+ const out = [];+ for (const kind of ["video", "audio"]) {+ const n = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();+ for (let ti = 0; ti < n; ti++) {+ if (kind === target.kind && ti === target.trackIndex) continue;+ const clips = await trackClips(await getTrack(seq, kind, ti));+ for (let i = 0; i < clips.length; i++) {+ const c = clips[i];+ if ((await c.getStartTime()).ticks !== S || (await c.getEndTime()).ticks !== E) continue;+ const cpi = await c.getProjectItem().catch(() => null);+ if (id && cpi && safe(() => cpi.getId()) === id) out.push({ clip: c, kind, trackIndex: ti, index: i, ref: `${trackLabel(kind, ti)}:${i}` });+ }+ }+ }+ return out;+}++/** Targets plus their linked partners (unless args.linked === false), de-duplicated. */+async function withLinked(seq, args, targets) {+ if (args.linked === false) return targets;+ const all = [...targets];+ for (const t of targets) {+ for (const p of await linkedPartners(seq, t)) if (!all.some((x) => x.ref === p.ref)) all.push(p);+ }+ return all;+}++/**+ * Run `fn(selection)` with a TrackItemSelection holding `clips`. The selection object is+ * only valid INSIDE createEmptySelection's callback ("The script object is no longer+ * valid" otherwise), so everything that uses it happens in there.+ */+function withSelection(clips, fn) {+ let result;+ let err = null;+ const ok = ppro.TrackItemSelection.createEmptySelection((sel) => {+ try {+ for (const c of clips) sel.addItem(c, true);+ result = fn(sel);+ } catch (e) {+ err = e;+ }+ });+ if (err) throw err;+ if (ok === false) throw fail("selection_failed", "Couldn't create a track item selection");+ return result;+}++async function resolveClips(seq, args) {+ const refs = args.clips || (args.clip !== undefined ? [args.clip] : null);+ if (!refs) throw fail("bad_args", "clip or clips is required");+ const out = [];+ for (const r of refs) out.push(await findClip(seq, r));+ return out;+}++// ───────────────────────────── helpers: components & params ─────────────────────────────++async function componentsOf(clip) {+ const chain = await clip.getComponentChain();+ const comps = [];+ for (let i = 0; i < chain.getComponentCount(); i++) {+ const c = chain.getComponentAtIndex(i);+ comps.push({ index: i, comp: c, displayName: await c.getDisplayName(), matchName: await c.getMatchName() });+ }+ return { chain, comps };+}++async function findComponent(clip, ref) {+ const { chain, comps } = await componentsOf(clip);+ const r = ref === undefined ? 0 : ref;+ const found =+ typeof r === "number"+ ? comps[r]+ : comps.find((c) => c.matchName === r || c.displayName.toLowerCase() === String(r).toLowerCase());+ if (!found) {+ throw fail("component_not_found", `No component '${ref}' on this clip. It has: ${comps.map((c) => `${c.displayName} (${c.matchName})`).join(", ")}`);+ }+ return { chain, ...found };+}++function findParam(comp, ref) {+ const n = comp.getParamCount();+ const names = [];+ for (let i = 0; i < n; i++) {+ const p = comp.getParam(i);+ names.push(p.displayName);+ if ((typeof ref === "number" && ref === i) || (typeof ref === "string" && p.displayName.toLowerCase() === ref.toLowerCase())) {+ return { param: p, index: i };+ }+ }+ throw fail("param_not_found", `No parameter '${ref}'. Parameters: ${names.join(", ")}`);+}++/**+ * Keyframe positions live in the clip's MEDIA time (timeline time - clip start + clip in+ * point), measured on 26.5. Verbs take sequence time by default (timeBase:"clip" opts out).+ */+async function kfTime(clip, v, seq, timeBase) {+ const t = await toTick(v, seq);+ if (timeBase === "clip") return t;+ return t.subtract(await clip.getStartTime()).add(await clip.getInPoint());+}++async function kfToSeq(clip, t) {+ return t.subtract(await clip.getInPoint()).add(await clip.getStartTime());+}++function makeValue(v) {+ if (Array.isArray(v) && v.length === 2) return new ppro.PointF(Number(v[0]), Number(v[1]));+ if (v && typeof v === "object" && "x" in v && "y" in v) return new ppro.PointF(Number(v.x), Number(v.y));+ if (v && typeof v === "object" && "red" in v) return new ppro.Color(v.red, v.green, v.blue, v.alpha === undefined ? 1 : v.alpha);+ return v;+}++async function paramValueAt(param, t) {+ try {+ const r = await param.getValueAtTime(t);+ return r && r.value !== undefined ? r.value : r;+ } catch (_) {+ try {+ const k = await param.getStartValue();+ return k && k.value !== undefined ? (k.value.value !== undefined ? k.value.value : k.value) : k;+ } catch (_) {+ return null;+ }+ }+}++// Premiere's clip "Volume > Level" param stores linear amplitude; the UI shows dB with+// 0 dB at the param's 1.0-equivalent. These match the long-standing ExtendScript mapping+// (level = 10^((dB-15)/20)); premiere_audio_probe verifies it on the running build.+function dbToLevel(db) {+ return Math.pow(10, (Number(db) - 15) / 20);+}+function levelToDb(level) {+ return level > 0 ? 20 * Math.log10(level) + 15 : -Infinity;+}++async function volumeParam(clip) {+ const { comps } = await componentsOf(clip);+ const vol = comps.find((c) => /volume/i.test(c.displayName) || /ADBE Volume/i.test(c.matchName));+ if (!vol) throw fail("no_volume", "This clip has no Volume component — is it an audio clip? Use an A-track ref like \"A1:0\".");+ const { param } = findParam(vol.comp, "Level");+ return param;+}++// ───────────────────────────── handlers ─────────────────────────────++const handlers = {+ // ── app / project ──+ async ping() {+ return { pong: true, ...(await helloInfo()) };+ },++ async get_project() {+ const p = await project();+ const seqs = await p.getSequences();+ const active = await p.getActiveSequence();+ const recs = await allItems(p);+ return {+ name: p.name,+ path: p.path,+ guid: p.guid.toString(),+ activeSequence: active ? active.name : null,+ sequences: seqs.map((s) => ({ name: s.name, guid: s.guid.toString() })),+ itemCount: recs.length,+ bins: recs.filter((r) => r.type === "bin").map((r) => (r.bin ? `${r.bin}/${r.name}` : r.name)),+ };+ },++ async open_project(a) {+ if (!a.path) throw fail("bad_args", "path is required");+ const opts = new ppro.OpenProjectOptions();+ safe(() => opts.setShowConvertProjectDialog(false));+ safe(() => opts.setShowLocateFileDialog(false));+ safe(() => opts.setShowWarningDialog(false));+ safe(() => opts.setAddToMRUList(true));+ const p = await ppro.Project.open(a.path, opts);+ if (!p) throw fail("open_failed", `Premiere couldn't open ${a.path}`);+ return { name: p.name, path: p.path };+ },++ async new_project(a) {+ if (!a.path) throw fail("bad_args", "path is required (…/MyProject.prproj)");+ const path = /\.prproj$/i.test(a.path) ? a.path : a.path + ".prproj";+ const p = await ppro.Project.createProject(path);+ if (!p) throw fail("create_failed", `Couldn't create ${path} — does the folder exist?`);+ return { name: p.name, path: p.path };+ },++ async save_project(a) {+ const p = await project();+ if (a.path) return { saved: await p.saveAs(a.path), path: a.path };+ return { saved: await p.save(), path: p.path };+ },++ async close_project(a) {+ const p = await project();+ if (a.save) await p.save();+ const opts = new ppro.CloseProjectOptions();+ safe(() => opts.setPromptIfDirty(false));+ safe(() => opts.setShowCancelButton(false));+ return { closed: await p.close(opts) };+ },++ // ── project panel ──+ async list_project_items(a) {+ const p = await project();+ let recs = await allItems(p);+ if (a.bin) recs = recs.filter((r) => r.bin === a.bin || r.bin.startsWith(a.bin + "/"));+ if (a.type) recs = recs.filter((r) => r.type === a.type);+ if (a.search) {+ const q = String(a.search).toLowerCase();+ recs = recs.filter((r) => r.name.toLowerCase().includes(q));+ }+ const limit = a.limit || 500;+ const items = [];+ for (const r of recs.slice(0, limit)) items.push(await describeItem(r, a.details !== false));+ return { count: recs.length, items, truncated: recs.length > limit };+ },++ async import_media(a) {+ const paths = a.paths || (a.path ? [a.path] : null);+ if (!paths || !paths.length) throw fail("bad_args", "paths (array of absolute desktop file paths) is required");+ const p = await project();+ const bin = await binFor(p, a.bin);+ const before = new Set((await allItems(p)).map((r) => r.id));+ const ok = await p.importFiles(paths, true, bin, !!a.asNumberedStills);+ const after = await allItems(p);+ const added = [];+ for (const r of after) if (!before.has(r.id)) added.push(await describeItem(r, true));+ if (!ok && !added.length) throw fail("import_failed", `Premiere refused the import. Check the paths exist on THIS machine: ${paths.join(", ")}`);+ return { imported: added.length, items: added };+ },++ async create_bin(a) {+ const p = await project();+ const parent = await binFor(p, a.parent);+ tx(p, "Create Bin", () => parent.createBinAction(a.name, a.makeUnique !== false));+ return { created: a.name, parent: a.parent || "/" };+ },++ async move_item(a) {+ const p = await project();+ const item = await findItem(p, a.item);+ const dest = await binFor(p, a.bin);+ const parent = item.getParentBin();+ tx(p, "Move Item", () => parent.createMoveItemAction(item, dest));+ return { moved: item.name, to: a.bin || "/" };+ },++ async rename_item(a) {+ const p = await project();+ const item = await findItem(p, a.item);+ tx(p, "Rename Item", () => item.createSetNameAction(a.name));+ return { renamed: a.name };+ },++ async delete_item(a) {+ const p = await project();+ const item = await findItem(p, a.item);+ const parent = item.getParentBin();+ tx(p, "Delete Item", () => parent.createRemoveItemAction(item));+ return { deleted: item.name };+ },++ async set_item_color(a) {+ const p = await project();+ const item = await findItem(p, a.item);+ tx(p, "Label Color", () => item.createSetColorLabelAction(Number(a.colorIndex)));+ return { item: item.name, colorIndex: a.colorIndex };+ },++ async set_source_in_out(a) {+ const p = await project();+ const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));+ if (!clip) throw fail("not_a_clip", "item is not a clip");+ if (a.clear) {+ tx(p, "Clear In/Out", () => clip.createClearInOutPointsAction());+ return { cleared: true };+ }+ const tin = await toTick(a.in, null);+ const tout = await toTick(a.out, null);+ tx(p, "Source In/Out", () => {+ if (tin && tout) return clip.createSetInOutPointsAction(tin, tout);+ if (tin) return clip.createSetInPointAction(tin);+ if (tout) return clip.createSetOutPointAction(tout);+ return [];+ });+ return { item: clip.name, in: a.in, out: a.out };+ },++ async create_subclip(a) {+ const p = await project();+ const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));+ const s = await toTick(a.start, null);+ const e = await toTick(a.end, null);+ tx(p, "Subclip", () => clip.createSubClipAction(a.name || `${clip.name} subclip`, s, e, !!a.hardBoundaries, { takeVideo: a.video !== false, takeAudio: a.audio !== false }));+ return { created: a.name || `${clip.name} subclip` };+ },++ async get_metadata(a) {+ const p = await project();+ const item = await findItem(p, a.item);+ return {+ projectMetadata: await ppro.Metadata.getProjectMetadata(item).catch(() => null),+ xmp: a.xmp ? await ppro.Metadata.getXMPMetadata(item).catch(() => null) : undefined,+ };+ },++ async set_xmp_metadata(a) {+ const p = await project();+ const item = await findItem(p, a.item);+ tx(p, "Set Metadata", () => ppro.Metadata.createSetXMPMetadataAction(item, a.xmp));+ return { item: item.name };+ },++ // ── sequences ──+ async list_sequences() {+ const p = await project();+ const active = await p.getActiveSequence();+ const out = [];+ for (const s of await p.getSequences()) {+ out.push({+ name: s.name,+ guid: s.guid.toString(),+ active: !!active && active.guid.toString() === s.guid.toString(),+ duration: round((await s.getEndTime()).seconds),+ });+ }+ return { sequences: out };+ },++ async set_active_sequence(a) {+ const p = await project();+ const s = await findSequence(p, a.sequence);+ await p.openSequence(s).catch(() => {});+ await p.setActiveSequence(s);+ return { active: s.name };+ },++ async create_sequence(a) {+ const p = await project();+ let s;+ if (a.fromItems && a.fromItems.length) {+ // Takes plain ProjectItems; passing an undefined bin is an "Illegal Parameter type".+ const items = [];+ for (const r of a.fromItems) items.push(await findItem(p, r));+ s = a.bin ? await p.createSequenceFromMedia(a.name || "Sequence", items, await binFor(p, a.bin)) : await p.createSequenceFromMedia(a.name || "Sequence", items);+ } else if (a.presetPath) {+ s = await p.createSequenceWithPresetPath(a.name || "Sequence", a.presetPath);+ } else {+ s = await p.createSequence(a.name || "Sequence");+ }+ if (!s) throw fail("create_failed", "Premiere didn't create the sequence");+ await p.setActiveSequence(s).catch(() => {});+ return { name: s.name, guid: s.guid.toString() };+ },++ async duplicate_sequence(a) {+ const { p, seq } = await seqCtx(a);+ tx(p, "Duplicate Sequence", () => seq.createCloneAction());+ return { duplicated: seq.name };+ },++ async delete_sequence(a) {+ const { p, seq } = await seqCtx(a);+ return { deleted: await p.deleteSequence(seq), name: seq.name };+ },++ async nest_selection(a) {+ const { seq } = await seqCtx(a);+ const nested = await seq.createSubsequence(!!a.ignoreTrackTargeting);+ return { nested: nested ? nested.name : null };+ },++ async get_sequence(a) {+ const { seq } = await seqCtx(a);+ const settings = await seq.getSettings();+ const fr = settings.getVideoFrameRate();+ const rect = await seq.getFrameSize();+ const out = {+ name: seq.name,+ guid: seq.guid.toString(),+ width: rect.width,+ height: rect.height,+ fps: round(fr.value || TICKS_PER_SECOND / fr.ticksPerFrame),+ duration: await timeOut(await seq.getEndTime(), seq),+ playhead: await timeOut(await seq.getPlayerPosition(), seq),+ inPoint: await timeOut(await seq.getInPoint(), seq),+ outPoint: await timeOut(await seq.getOutPoint(), seq),+ videoTracks: [],+ audioTracks: [],+ };+ const detail = a.detail !== false;+ for (const kind of ["video", "audio"]) {+ const n = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();+ for (let t = 0; t < n; t++) {+ const track = await getTrack(seq, kind, t);+ const clips = await trackClips(track);+ const desc = [];+ for (let i = 0; i < clips.length; i++) desc.push(await describeClip(clips[i], seq, kind, t, i, detail));+ (kind === "video" ? out.videoTracks : out.audioTracks).push({+ track: trackLabel(kind, t),+ name: track.name,+ muted: await track.isMuted().catch(() => false),+ clips: desc,+ });+ }+ }+ return out;+ },++ async get_sequence_settings(a) {+ const { seq } = await seqCtx(a);+ const s = await seq.getSettings();+ const fr = s.getVideoFrameRate();+ const rect = await s.getVideoFrameRect();+ return {+ width: rect.width,+ height: rect.height,+ fps: round(fr.value),+ pixelAspectRatio: await s.getVideoPixelAspectRatio().catch(() => null),+ fieldType: await s.getVideoFieldType().catch(() => null),+ audioSampleRate: await s.getAudioSampleRate().then((r) => r.value).catch(() => null),+ audioChannelCount: await s.getAudioChannelCount().catch(() => null),+ editingMode: await s.getEditingMode().catch(() => null),+ previewCodec: await s.getPreviewCodec().catch(() => null),+ maxBitDepth: await s.getMaximumBitDepth().catch(() => null),+ maxRenderQuality: await s.getMaxRenderQuality().catch(() => null),+ };+ },++ async set_sequence_settings(a) {+ const { p, seq } = await seqCtx(a);+ const s = await seq.getSettings();+ if (a.width && a.height) {+ const r = new ppro.RectF();+ r.width = a.width;+ r.height = a.height;+ await s.setVideoFrameRect(r);+ }+ if (a.fps) s.setVideoFrameRate(ppro.FrameRate.createWithValue(Number(a.fps)));+ if (a.maxBitDepth !== undefined) await s.setMaximumBitDepth(!!a.maxBitDepth);+ if (a.maxRenderQuality !== undefined) await s.setMaxRenderQuality(!!a.maxRenderQuality);+ tx(p, "Sequence Settings", () => seq.createSetSettingsAction(s));+ return await handlers.get_sequence_settings(a);+ },++ async set_playhead(a) {+ const { seq } = await seqCtx(a);+ const t = await toTick(a.time, seq);+ await seq.setPlayerPosition(t);+ return { playhead: await timeOut(await seq.getPlayerPosition(), seq) };+ },++ async set_in_out(a) {+ const { p, seq } = await seqCtx(a);+ const tin = await toTick(a.in, seq);+ const tout = await toTick(a.out, seq);+ tx(p, "Sequence In/Out", () => [tin && seq.createSetInPointAction(tin), tout && seq.createSetOutPointAction(tout)]);+ if (a.workArea) ppro.WorkAreaUtils.setWorkAreaInOutPoints(seq, tin, tout);+ return { inPoint: await timeOut(await seq.getInPoint(), seq), outPoint: await timeOut(await seq.getOutPoint(), seq) };+ },++ // ── timeline editing ──+ async insert_clip(a) {+ return placeClip(a, "insert");+ },+ async overwrite_clip(a) {+ return placeClip(a, "overwrite");+ },+ async append_clip(a) {+ const { seq } = await seqCtx(a);+ const { kind, index } = parseTrack(a.track || "V1");+ let end = 0;+ const clips = await trackClips(await getTrack(seq, kind, index));+ if (clips.length) end = (await clips[clips.length - 1].getEndTime()).seconds;+ return placeClip({ ...a, time: end }, "overwrite");+ },++ async move_clip(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await withLinked(seq, a, await resolveClips(seq, a));+ const plans = [];+ for (const t of targets) {+ const start = await t.clip.getStartTime();+ let delta;+ if (a.by !== undefined) delta = await toTick(a.by, seq);+ else if (a.to !== undefined) delta = (await toTick(a.to, seq)).subtract(start);+ else throw fail("bad_args", "Pass to (new start time) or by (offset, may be negative)");+ plans.push({ clip: t.clip, delta });+ }+ tx(p, "Move Clip", () => plans.map((x) => x.clip.createMoveAction(x.delta)));+ return { moved: targets.map((t) => t.ref), _note: "Moving can overwrite neighbors; call premiere_get_sequence to see the result." };+ },++ async trim_clip(a) {+ const { p, seq } = await seqCtx(a);+ const target = await findClip(seq, a.clip);+ const { clip, ref, kind, trackIndex, index } = target;+ const s0 = await toTick(a.start, seq);+ const e0 = await toTick(a.end, seq);+ const i = await toTick(a.inPoint, seq);+ const o = await toTick(a.outPoint, seq);+ const ds = a.trimStart !== undefined ? (await clip.getStartTime()).add(await toTick(a.trimStart, seq)) : null;+ const de = a.trimEnd !== undefined ? (await clip.getEndTime()).subtract(await toTick(a.trimEnd, seq)) : null;+ const group = await withLinked(seq, a, [target]);+ tx(p, "Trim Clip", () =>+ group.flatMap((g) => [+ (s0 || ds) && g.clip.createSetStartAction(s0 || ds),+ (e0 || de) && g.clip.createSetEndAction(e0 || de),+ i && g.clip.createSetInPointAction(i),+ o && g.clip.createSetOutPointAction(o),+ ])+ );+ return { clip: ref, linked: group.slice(1).map((g) => g.ref), after: await describeClip(clip, seq, kind, trackIndex, index, false) };+ },++ async remove_clips(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await withLinked(seq, a, await resolveClips(seq, a));+ const ed = ppro.SequenceEditor.getEditor(seq);+ withSelection(targets.map((t) => t.clip), (sel) =>+ tx(p, a.ripple ? "Ripple Delete" : "Delete Clips", () => ed.createRemoveItemsAction(sel, !!a.ripple, C.MediaType.ANY, !!a.shiftOverlapping))+ );+ return { removed: targets.map((t) => t.ref), ripple: !!a.ripple };+ },++ async remove_gap(a) {+ // Ripple-close the gap before a clip: it and everything after it on its track (and on+ // its linked partners' tracks, unless linked:false) shift left by the gap.+ const { p, seq } = await seqCtx(a);+ const target = await findClip(seq, a.clip);+ const { clip, kind, trackIndex, index } = target;+ const clips = await trackClips(await getTrack(seq, kind, trackIndex));+ const prevEnd = index > 0 ? await clips[index - 1].getEndTime() : ppro.TickTime.TIME_ZERO;+ const start = await clip.getStartTime();+ const gap = start.subtract(prevEnd);+ if (gap.ticksNumber <= 0) return { gap: 0 };+ const neg = ppro.TickTime.TIME_ZERO.subtract(gap);+ const moving = clips.slice(index);+ for (const partner of a.linked === false ? [] : await linkedPartners(seq, target)) {+ const pcs = await trackClips(await getTrack(seq, partner.kind, partner.trackIndex));+ const pPrevEnd = partner.index > 0 ? await pcs[partner.index - 1].getEndTime() : ppro.TickTime.TIME_ZERO;+ if (start.subtract(pPrevEnd).ticksNumber < gap.ticksNumber) {+ throw fail("gap_blocked", `${partner.ref} can't shift left by ${round(gap.seconds)}s without overlapping the clip before it. Pass linked:false to close the gap on ${trackLabel(kind, trackIndex)} only.`);+ }+ moving.push(...pcs.slice(partner.index));+ }+ tx(p, "Close Gap", () => moving.map((c) => c.createMoveAction(neg)));+ return { closed: round(gap.seconds), movedClips: moving.length };+ },++ async set_clip_enabled(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await resolveClips(seq, a);+ tx(p, "Enable/Disable", () => targets.map((t) => t.clip.createSetDisabledAction(a.enabled === false)));+ return { clips: targets.map((t) => t.ref), enabled: a.enabled !== false };+ },++ async rename_clip(a) {+ const { p, seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ tx(p, "Rename Clip", () => clip.createSetNameAction(a.name));+ return { clip: ref, name: a.name };+ },++ async duplicate_clip(a) {+ const { p, seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const off = (await toTick(a.offset === undefined ? 0 : a.offset, seq)) || ppro.TickTime.TIME_ZERO;+ const ed = ppro.SequenceEditor.getEditor(seq);+ tx(p, "Duplicate Clip", () => ed.createCloneTrackItemAction(clip, off, Number(a.videoTrackOffset || 0), Number(a.audioTrackOffset || 0), a.alignToVideo !== false, !!a.insert));+ return { duplicated: ref };+ },++ async razor(a, ctx) {+ // UXP has no razor. Measured on 26.5: head trims (setStart/setInPoint) keep a clip's+ // sync offset (start - in), tail trims only move the end, and any cloned/moved copy+ // breaks sync — so a trim+clone razor can't be correct. Instead, for each linked+ // V/A group spanning T: trim the tails to T, temporarily set the SOURCE clip's in/out+ // to the right-hand half, overwrite it back in at T (same sync offset), then restore+ // the source in/out. Effects on the original stay on the left half only; the CEP+ // helper's true QE razor (preferred when connected) keeps them on both.+ const { p, seq } = await seqCtx(a);+ const fr = await frameRate(seq);+ let T = await toTick(a.time, seq);+ if (T.alignToNearestFrame) T = T.alignToNearestFrame(fr);+ const want = a.tracks ? new Set(a.tracks.map((t) => String(t).toUpperCase())) : null;+ const spans = [];+ for (const kind of ["video", "audio"]) {+ const n = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();+ for (let ti = 0; ti < n; ti++) {+ if (want && !want.has(trackLabel(kind, ti))) continue;+ for (const c of await trackClips(await getTrack(seq, kind, ti))) {+ const S = await c.getStartTime();+ const E = await c.getEndTime();+ if (T.ticksNumber > S.ticksNumber && T.ticksNumber < E.ticksNumber) {+ spans.push({ c, kind, ti, S, E, I: await c.getInPoint(), speed: await c.getSpeed().catch(() => 1), pi: await c.getProjectItem() });+ }+ }+ }+ }+ if (!spans.length) {+ ctx.warn("No clip spans that time on the chosen tracks — nothing was cut.");+ return { cutTracks: [], at: await timeOut(T, seq), method: "uxp-overwrite" };+ }+ // Group linked partners: same source item, same start/end.+ const groups = [];+ for (const sp of spans) {+ const key = `${sp.pi && safe(() => sp.pi.getId())}|${sp.S.ticks}|${sp.E.ticks}`;+ let g = groups.find((x) => x.key === key);+ if (!g) groups.push((g = { key, members: [] }));+ g.members.push(sp);+ }+ const ed = ppro.SequenceEditor.getEditor(seq);+ const cut = [];+ for (const g of groups) {+ const ref = g.members[0];+ if (Math.abs(ref.speed - 1) > 1e-6) throw fail("razor_speed", "This clip has a speed change; the UXP razor fallback can't keep sync. Connect the CEP helper (premiere_install_plugin + restart Premiere) for a true razor.");+ const clipItem = ppro.ClipProjectItem.cast(ref.pi);+ if (!clipItem) throw fail("razor_failed", "Can't razor a clip without a source project item (e.g. a graphic) in UXP mode.");+ const v = g.members.find((m) => m.kind === "video");+ const au = g.members.find((m) => m.kind === "audio");+ const vt = v ? v.ti : -1;+ const at = au ? au.ti : -1;+ const left = T.subtract(ref.S);+ const newIn = ref.I.add(left);+ const newOut = ref.I.add(ref.E.subtract(ref.S));+ const mtV = C.MediaType.VIDEO;+ const prevIn = await clipItem.getInPoint(v ? mtV : C.MediaType.AUDIO).catch(() => null);+ const prevOut = await clipItem.getOutPoint(v ? mtV : C.MediaType.AUDIO).catch(() => null);+ tx(p, "Razor (trim)", () => g.members.map((m) => m.c.createSetEndAction(T)));+ try {+ tx(p, "Razor (source range)", () => clipItem.createSetInOutPointsAction(newIn, newOut));+ // Missing V or A in the group: aim that half at a far track index Premiere ignores is+ // not available, so overwrite both and then remove the stray half below.+ tx(p, "Razor (overwrite)", () => ed.createOverwriteItemAction(ref.pi, T, vt >= 0 ? vt : 0, at >= 0 ? at : 0));+ } finally {+ tx(p, "Razor (restore source)", () => (prevIn && prevOut ? clipItem.createSetInOutPointsAction(prevIn, prevOut) : clipItem.createClearInOutPointsAction()));+ }+ // If only one side was razored, the overwrite also laid the other side down at T on+ // track 0 — remove that stray piece so other tracks are untouched.+ if (vt < 0 || at < 0) {+ const strayKind = vt < 0 ? "video" : "audio";+ const strayTrack = await getTrack(seq, strayKind, 0);+ const stray = await clipStartingAt(strayTrack, T);+ if (stray) {+ withSelection([stray], (sel) => tx(p, "Razor (cleanup)", () => ed.createRemoveItemsAction(sel, false, C.MediaType.ANY, false)));+ ctx.warn(`Overwrite also touched ${trackLabel(strayKind, 0)} at the cut; that piece was removed. Razor linked V+A together to avoid this.`);+ }+ }+ for (const m of g.members) cut.push(trackLabel(m.kind, m.ti));+ }+ return { cutTracks: cut, at: await timeOut(T, seq), method: "uxp-overwrite" };+ },++ async select_clips(a) {+ const { seq } = await seqCtx(a);+ const targets = await withLinked(seq, a, await resolveClips(seq, a));+ withSelection(targets.map((t) => t.clip), (sel) => seq.setSelection(sel));+ return { selected: targets.map((t) => t.ref) };+ },++ async get_selection(a) {+ const { seq } = await seqCtx(a);+ const sel = await seq.getSelection();+ const items = await sel.getTrackItems();+ const out = [];+ for (const it of items) out.push({ name: await it.getName(), start: await timeOut(await it.getStartTime(), seq), end: await timeOut(await it.getEndTime(), seq), track: await it.getTrackIndex() });+ return { count: out.length, items: out };+ },++ async clear_selection(a) {+ const { seq } = await seqCtx(a);+ return { cleared: await seq.clearSelection() };+ },++ async scene_edit_detection(a) {+ const { seq } = await seqCtx(a);+ const targets = await resolveClips(seq, a);+ const op = { cut: ppro.SequenceUtils.SEQUENCE_OPERATION_APPLYCUT, marker: ppro.SequenceUtils.SEQUENCE_OPERATION_CREATEMARKER, subclip: ppro.SequenceUtils.SEQUENCE_OPERATION_CREATESUBCLIP }[a.operation || "cut"];+ const started = await withSelection(targets.map((t) => t.clip), (sel) => ppro.SequenceUtils.performSceneEditDetectionOnSelection(op, sel));+ return { started, operation: a.operation || "cut" };+ },++ async set_track_mute(a) {+ const { seq } = await seqCtx(a);+ const { kind, index } = parseTrack(a.track);+ const track = await getTrack(seq, kind, index);+ await track.setMute(a.mute !== false);+ return { track: trackLabel(kind, index), muted: await track.isMuted() };+ },++ async rename_track(a) {+ const { p, seq } = await seqCtx(a);+ const { kind, index } = parseTrack(a.track);+ const track = await getTrack(seq, kind, index);+ tx(p, "Rename Track", () => track.createSetNameAction(a.name));+ return { track: trackLabel(kind, index), name: a.name };+ },++ // ── effects, params, keyframes ──+ async list_effects(a) {+ const kind = a.kind || "video";+ let names;+ if (kind === "video") {+ const display = await ppro.VideoFilterFactory.getDisplayNames();+ const match = await ppro.VideoFilterFactory.getMatchNames();+ names = match.map((m, i) => ({ matchName: m, displayName: display[i] }));+ } else if (kind === "audio") {+ names = (await ppro.AudioFilterFactory.getDisplayNames()).map((d) => ({ displayName: d }));+ } else if (kind === "transition") {+ names = (await ppro.TransitionFactory.getVideoTransitionMatchNames()).map((m) => ({ matchName: m }));+ } else throw fail("bad_args", "kind must be video, audio, or transition");+ if (a.search) {+ const q = String(a.search).toLowerCase();+ names = names.filter((n) => (n.displayName || "").toLowerCase().includes(q) || (n.matchName || "").toLowerCase().includes(q));+ }+ return { kind, count: names.length, effects: names };+ },++ async get_clip_effects(a) {+ const { seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const { comps } = await componentsOf(clip);+ const at = a.time !== undefined ? await kfTime(clip, a.time, seq, a.timeBase) : await clip.getInPoint();+ const out = [];+ for (const c of comps) {+ const params = [];+ for (let i = 0; i < c.comp.getParamCount(); i++) {+ const prm = c.comp.getParam(i);+ params.push({+ index: i,+ name: prm.displayName,+ value: await paramValueAt(prm, at),+ animated: safe(() => prm.isTimeVarying(), false),+ keyframes: await Promise.all(safe(() => prm.getKeyframeListAsTickTimes(), []).map(async (t) => round((await kfToSeq(clip, t)).seconds))),+ });+ }+ out.push({ index: c.index, displayName: c.displayName, matchName: c.matchName, params });+ }+ return { clip: ref, components: out };+ },++ async add_effect(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await resolveClips(seq, a);+ const done = [];+ for (const t of targets) {+ let comp;+ if (t.kind === "video") {+ let match = a.matchName;+ if (!match && a.name) {+ const display = await ppro.VideoFilterFactory.getDisplayNames();+ const matches = await ppro.VideoFilterFactory.getMatchNames();+ const i = display.findIndex((d) => d.toLowerCase() === String(a.name).toLowerCase());+ if (i < 0) throw fail("effect_not_found", `No video effect named '${a.name}'. premiere_list_effects {kind:"video", search:"…"} lists them.`);+ match = matches[i];+ }+ comp = await ppro.VideoFilterFactory.createComponent(match);+ } else {+ comp = await ppro.AudioFilterFactory.createComponentByDisplayName(a.name || a.matchName, t.clip);+ }+ if (!comp) throw fail("effect_not_found", `Premiere couldn't create effect '${a.name || a.matchName}'`);+ const chain = await t.clip.getComponentChain();+ tx(p, "Add Effect", () => (a.index !== undefined ? chain.createInsertComponentAction(comp, Number(a.index)) : chain.createAppendComponentAction(comp)));+ done.push(t.ref);+ }+ return { added: a.name || a.matchName, clips: done };+ },++ async remove_effect(a) {+ const { p, seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const { chain, comp, displayName } = await findComponent(clip, a.component);+ tx(p, "Remove Effect", () => chain.createRemoveComponentAction(comp));+ return { clip: ref, removed: displayName };+ },++ async set_param(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await resolveClips(seq, a);+ const results = [];+ for (const t of targets) {+ const { comp, displayName } = await findComponent(t.clip, a.component);+ const { param } = findParam(comp, a.param);+ const kf = param.createKeyframe(makeValue(a.value));+ if (a.time !== undefined) {+ kf.position = await kfTime(t.clip, a.time, seq, a.timeBase);+ const animated = safe(() => param.isTimeVarying(), false);+ tx(p, "Keyframe", () => [!animated && param.createSetTimeVaryingAction(true), param.createAddKeyframeAction(kf)]);+ } else {+ tx(p, "Set Param", () => param.createSetValueAction(kf, true));+ }+ results.push({ clip: t.ref, component: displayName, param: param.displayName, value: a.value, keyframedAt: a.time });+ }+ return { set: results };+ },++ async add_keyframes(a) {+ const { p, seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const { comp } = await findComponent(clip, a.component);+ const { param } = findParam(comp, a.param);+ if (!Array.isArray(a.keyframes) || !a.keyframes.length) throw fail("bad_args", "keyframes: [{time, value}, …] is required");+ const kfs = [];+ for (const k of a.keyframes) {+ const kf = param.createKeyframe(makeValue(k.value));+ kf.position = await kfTime(clip, k.time, seq, a.timeBase);+ kfs.push(kf);+ }+ const interp = a.interpolation ? { linear: ppro.Keyframe.INTERPOLATION_MODE_LINEAR, hold: ppro.Keyframe.INTERPOLATION_MODE_HOLD, bezier: ppro.Keyframe.INTERPOLATION_MODE_BEZIER }[a.interpolation] : undefined;+ const animated = safe(() => param.isTimeVarying(), false);+ tx(p, "Add Keyframes", () => {+ const acts = [];+ if (!animated) acts.push(param.createSetTimeVaryingAction(true));+ for (const kf of kfs) acts.push(param.createAddKeyframeAction(kf));+ if (interp !== undefined) for (const kf of kfs) acts.push(param.createSetInterpolationAtKeyframeAction(kf.position, interp, true));+ return acts;+ });+ return { clip: ref, param: param.displayName, keyframes: a.keyframes.length };+ },++ async remove_keyframes(a) {+ const { p, seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const { comp } = await findComponent(clip, a.component);+ const { param } = findParam(comp, a.param);+ if (a.all) {+ tx(p, "Clear Keyframes", () => param.createSetTimeVaryingAction(false));+ return { clip: ref, cleared: true };+ }+ const s = await kfTime(clip, a.start !== undefined ? a.start : a.time, seq, a.timeBase);+ const e = a.end !== undefined ? await kfTime(clip, a.end, seq, a.timeBase) : null;+ tx(p, "Remove Keyframes", () => (e ? param.createRemoveKeyframeRangeAction(s, e, true) : param.createRemoveKeyframeAction(s, true)));+ return { clip: ref, removed: true };+ },++ async set_transform(a) {+ // Convenience over the built-in Motion / Opacity components.+ const { seq } = await seqCtx(a);+ const out = [];+ const set = async (component, param, value) => {+ if (value === undefined) return;+ await handlers.set_param({ ...a, component, param, value });+ out.push(`${component}.${param}=${JSON.stringify(value)}`);+ };+ await findClip(seq, a.clip);+ await set("Motion", "Scale", a.scale);+ await set("Motion", "Position", a.position);+ await set("Motion", "Rotation", a.rotation);+ await set("Motion", "Anchor Point", a.anchor);+ await set("Opacity", "Opacity", a.opacity);+ return { clip: a.clip, set: out };+ },++ async add_transition(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await resolveClips(seq, a);+ let match = a.matchName;+ if (!match) {+ const all = await ppro.TransitionFactory.getVideoTransitionMatchNames();+ const q = String(a.name || "Cross Dissolve").toLowerCase().replace(/\s+/g, "");+ match = all.find((m) => m.toLowerCase().replace(/\s+/g, "").includes(q));+ if (!match) throw fail("transition_not_found", `No transition like '${a.name}'. premiere_list_effects {kind:"transition"} lists match names.`);+ }+ const opts = new ppro.AddTransitionOptions();+ if (a.duration !== undefined) opts.setDuration(await toTick(a.duration, seq));+ opts.setApplyToStart(a.position === "start");+ if (a.singleSided) opts.setForceSingleSided(true);+ if (a.alignment !== undefined) opts.setTransitionAlignment(Number(a.alignment));+ const done = [];+ for (const t of targets) {+ if (t.kind !== "video") throw fail("video_only", "UXP only supports video transitions. For audio, use premiere_add_keyframes on Volume>Level to fade.");+ const tr = ppro.TransitionFactory.createVideoTransition(match);+ tx(p, "Add Transition", () => t.clip.createAddVideoTransitionAction(tr, opts));+ done.push(t.ref);+ }+ return { transition: match, clips: done, position: a.position || "end" };+ },++ async remove_transition(a) {+ const { p, seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const pos = a.position === "start" ? C.TransitionPosition.START : C.TransitionPosition.END;+ tx(p, "Remove Transition", () => clip.createRemoveVideoTransitionAction(pos));+ return { clip: ref, removed: a.position || "end" };+ },++ async insert_mogrt(a) {+ const { p, seq } = await seqCtx(a);+ const t = await toTick(a.time || 0, seq);+ const { index: v } = parseTrack(a.track || "V2");+ const ed = ppro.SequenceEditor.getEditor(seq);+ let items;+ p.lockedAccess(() => {+ items = a.path ? ed.insertMogrtFromPath(a.path, t, v, Number(a.audioTrack || 0)) : ed.insertMogrtFromLibrary(a.library, a.element, t, v, Number(a.audioTrack || 0));+ });+ return { inserted: (items || []).length, track: trackLabel("video", v), _note: "Edit its text with premiere_get_clip_effects then premiere_set_param on the Graphic's text param." };+ },++ // ── audio ──+ async set_clip_volume(a) {+ const { p, seq } = await seqCtx(a);+ const targets = await resolveClips(seq, a);+ const out = [];+ for (const t of targets) {+ if (t.kind !== "audio") throw fail("audio_only", `${t.ref} is a video clip — volume lives on the linked audio clip (A-track).`);+ const param = await volumeParam(t.clip);+ const level = a.level !== undefined ? Number(a.level) : dbToLevel(a.db === undefined ? 0 : a.db);+ const kf = param.createKeyframe(level);+ if (a.time !== undefined) {+ kf.position = await kfTime(t.clip, a.time, seq, a.timeBase);+ const animated = safe(() => param.isTimeVarying(), false);+ tx(p, "Volume Keyframe", () => [!animated && param.createSetTimeVaryingAction(true), param.createAddKeyframeAction(kf)]);+ } else {+ tx(p, "Clip Volume", () => param.createSetValueAction(kf, true));+ }+ out.push({ clip: t.ref, db: a.db, level });+ }+ return { set: out };+ },++ async audio_fade(a) {+ // Volume keyframes: fade in at clip start or fade out at clip end.+ const { p, seq } = await seqCtx(a);+ const { clip, ref, kind } = await findClip(seq, a.clip);+ if (kind !== "audio") throw fail("audio_only", "Use an audio clip ref (A1:0)");+ const param = await volumeParam(clip);+ const dur = await toTick(a.duration || 1, seq);+ // Media-time span of the clip (keyframes live in media time).+ const start = await clip.getInPoint();+ const end = start.add(await clip.getDuration());+ const full = dbToLevel(a.db === undefined ? 0 : a.db);+ const silent = dbToLevel(-96);+ const pts = a.type === "out" ? [[end.subtract(dur), full], [end, silent]] : [[start, silent], [start.add(dur), full]];+ const kfs = pts.map(([t, v]) => {+ const k = param.createKeyframe(v);+ k.position = t;+ return k;+ });+ const animated = safe(() => param.isTimeVarying(), false);+ tx(p, "Audio Fade", () => [!animated && param.createSetTimeVaryingAction(true), ...kfs.map((k) => param.createAddKeyframeAction(k))]);+ return { clip: ref, fade: a.type || "in", seconds: round(dur.seconds) };+ },++ async audio_probe(a) {+ const { seq } = await seqCtx(a);+ const { clip, ref } = await findClip(seq, a.clip);+ const param = await volumeParam(clip);+ const v = await paramValueAt(param, a.time !== undefined ? await kfTime(clip, a.time, seq) : await clip.getInPoint());+ return { clip: ref, levelRaw: v, dbEstimate: typeof v === "number" ? round(levelToDb(v)) : null };+ },++ // ── markers ──+ async list_markers(a) {+ const p = await project();+ const owner = a.item ? ppro.ClipProjectItem.cast(await findItem(p, a.item)) : await findSequence(p, a.sequence);+ const seq = a.item ? null : owner;+ const m = await ppro.Markers.getMarkers(owner);+ const out = [];+ for (const k of m.getMarkers()) {+ out.push({+ guid: k.guid.toString(),+ name: k.getName(),+ comments: k.getComments(),+ start: await timeOut(k.getStart(), seq),+ duration: round(k.getDuration().seconds),+ type: k.getType(),+ colorIndex: k.getColorIndex(),+ });+ }+ return { count: out.length, markers: out };+ },++ async add_marker(a) {+ const p = await project();+ const owner = a.item ? ppro.ClipProjectItem.cast(await findItem(p, a.item)) : await findSequence(p, a.sequence);+ const seq = a.item ? null : owner;+ const m = await ppro.Markers.getMarkers(owner);+ const t = a.time !== undefined ? await toTick(a.time, seq) : seq ? await seq.getPlayerPosition() : ppro.TickTime.TIME_ZERO;+ const d = a.duration !== undefined ? await toTick(a.duration, seq) : ppro.TickTime.TIME_ZERO;+ const type = { comment: ppro.Marker.MARKER_TYPE_COMMENT, chapter: ppro.Marker.MARKER_TYPE_CHAPTER, weblink: ppro.Marker.MARKER_TYPE_WEBLINK }[a.type || "comment"] || ppro.Marker.MARKER_TYPE_COMMENT;+ tx(p, "Add Marker", () => m.createAddMarkerAction(a.name || "", type, t, d, a.comments || ""));+ if (a.colorIndex !== undefined) {+ const m2 = await ppro.Markers.getMarkers(owner);+ const added = m2.getMarkers().find((k) => k.getStart().ticks === t.ticks && k.getName() === (a.name || ""));+ if (added) tx(p, "Marker Color", () => added.createSetColorByIndexAction(Number(a.colorIndex)));+ }+ return { added: a.name || "", at: await timeOut(t, seq) };+ },++ async remove_marker(a) {+ const p = await project();+ const owner = a.item ? ppro.ClipProjectItem.cast(await findItem(p, a.item)) : await findSequence(p, a.sequence);+ const m = await ppro.Markers.getMarkers(owner);+ const all = m.getMarkers();+ const targets = a.all ? all : all.filter((k) => k.guid.toString() === a.guid || (a.name !== undefined && k.getName() === a.name));+ if (!targets.length) throw fail("marker_not_found", "No matching marker (pass guid, name, or all:true)");+ tx(p, "Remove Marker", () => targets.map((k) => m.createRemoveMarkerAction(k)));+ return { removed: targets.length };+ },++ // ── preview & export ──+ async export_frame(a) {+ const { seq } = await seqCtx(a);+ const t = a.time !== undefined ? await toTick(a.time, seq) : await seq.getPlayerPosition();+ const size = await seq.getFrameSize();+ let w = Number(a.width || 0);+ let h = Number(a.height || 0);+ if (!w && !h) w = Math.min(size.width, 1280);+ if (w && !h) h = Math.round((size.height * w) / size.width);+ if (h && !w) w = Math.round((size.width * h) / size.height);+ const fmt = (a.format || "png").toLowerCase();+ const dir = a.dir;+ const base = a.name || `frame_${Math.round(t.seconds * 1000)}ms`;+ if (!dir) throw fail("bad_args", "dir is required");+ const ok = await ppro.Exporter.exportSequenceFrame(seq, t, `${base}.${fmt}`, dir, w, h);+ if (!ok) throw fail("export_failed", `exportSequenceFrame failed (dir ${dir})`);+ // Some builds append the extension again ("x.png.png"); report whichever file exists.+ const sep = dir.includes("\\") ? "\\" : "/";+ const stem = `${dir.replace(/[\\/]$/, "")}${sep}${base}.${fmt}`;+ let path = stem;+ for (const cand of [stem, `${stem}.${fmt}`]) {+ try {+ await fs.lstat(cand);+ path = cand;+ break;+ } catch (_) {}+ }+ return { path, time: await timeOut(t, seq), width: w, height: h };+ },++ async export_frames(a) {+ const { seq } = await seqCtx(a);+ let times = a.times;+ if (!times) {+ const end = (await seq.getEndTime()).seconds;+ const n = Math.max(1, Math.min(Number(a.count || 8), 64));+ const from = a.start !== undefined ? (await toTick(a.start, seq)).seconds : 0;+ const to = a.end !== undefined ? (await toTick(a.end, seq)).seconds : end;+ times = Array.from({ length: n }, (_, i) => from + ((to - from) * (i + 0.5)) / n);+ }+ const frames = [];+ for (let i = 0; i < times.length; i++) {+ frames.push(await handlers.export_frame({ ...a, time: times[i], name: `${a.prefix || "frame"}_${String(i).padStart(3, "0")}` }));+ }+ return { frames };+ },++ async export_sequence(a) {+ const { seq } = await seqCtx(a);+ if (!a.output) throw fail("bad_args", "output (absolute file path on this machine) is required");+ if (!a.preset) throw fail("bad_args", "preset (.epr path) is required — premiere_list_export_presets finds them");+ const em = ppro.EncoderManager.getManager();+ const mode = { immediate: C.ExportType.IMMEDIATELY, ame: C.ExportType.QUEUE_TO_AME, queue: C.ExportType.QUEUE_TO_APP }[a.mode || "immediate"];+ if (a.mode === "ame" && !em.isAMEInstalled) throw fail("ame_missing", "Adobe Media Encoder isn't installed; use mode:\"immediate\".");+ watchEncoder();+ const ok = await em.exportSequence(seq, mode, a.output, a.preset, a.range !== "inout");+ if (a.mode === "ame" && a.startQueue !== false) await em.startBatchEncode().catch(() => {});+ return { started: ok, output: a.output, mode: a.mode || "immediate" };+ },++ async export_interchange(a) {+ const { seq } = await seqCtx(a);+ if (!a.output) throw fail("bad_args", "output path is required");+ const f = a.format || "otio";+ let ok;+ if (f === "otio") ok = await ppro.ProjectConverter.exportAsOpenTimelineIO(seq, a.output, true);+ else if (f === "fcpxml") ok = await ppro.ProjectConverter.exportAsFinalCutProXML(seq, a.output, true);+ else if (f === "aaf") ok = await ppro.ProjectConverter.exportAAF(seq, a.output);+ else throw fail("bad_args", "format must be otio, fcpxml, or aaf");+ return { exported: ok, format: f, output: a.output };+ },++ async source_monitor(a) {+ const p = await project();+ if (a.close) return { closed: await ppro.SourceMonitor.closeAllClips() };+ if (a.path) await ppro.SourceMonitor.openFilePath(a.path);+ else if (a.item) await ppro.SourceMonitor.openProjectItem(await findItem(p, a.item));+ if (a.time !== undefined) await ppro.SourceMonitor.setPosition(await toTick(a.time, null));+ if (a.play) await ppro.SourceMonitor.play(Number(a.speed || 1));+ const pi = await ppro.SourceMonitor.getProjectItem().catch(() => null);+ return { item: pi ? pi.name : null, position: round((await ppro.SourceMonitor.getPosition()).seconds) };+ },++ // ── transcripts ──+ async transcribe(a) {+ const p = await project();+ const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));+ return { started: await ppro.Transcript.transcribeClipProjectItem(clip, a.language ? { language: a.language } : undefined) };+ },++ async get_transcript(a) {+ const p = await project();+ const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));+ if (!ppro.Transcript.hasTranscript(clip)) throw fail("no_transcript", "No transcript yet — premiere_transcribe first (takes a while).");+ const json = await ppro.Transcript.exportToJSON(clip);+ return { transcript: safe(() => JSON.parse(json), json) };+ },++ // ── escape hatch ──+ async eval(a) {+ if (!a.code) throw fail("bad_args", "code is required: the body of an async function. In scope: ppro, h (helpers), args. `return` a JSON-able value.");+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;+ const fn = new AsyncFunction("ppro", "h", "args", "require", a.code);+ const r = await fn(ppro, helpers, a.args || {}, require);+ return { value: toJsonSafe(r) };+ },+};++async function placeClip(a, mode) {+ const { p, seq } = await seqCtx(a);+ const item = await findItem(p, a.item);+ const t = a.time !== undefined ? await toTick(a.time, seq) : await seq.getPlayerPosition();+ const v = a.videoTrack !== undefined ? parseTrack(a.videoTrack).index : a.track ? parseTrack(a.track).index : 0;+ const au = a.audioTrack !== undefined ? parseTrack(a.audioTrack).index : v;+ const ed = ppro.SequenceEditor.getEditor(seq);+ tx(p, mode === "insert" ? "Insert Clip" : "Overwrite Clip", () =>+ mode === "insert" ? ed.createInsertProjectItemAction(item, t, v, au, !!a.limitShift) : ed.createOverwriteItemAction(item, t, v, au)+ );+ // Report the clip that now starts at t on the target video track (if any).+ let placed = null;+ try {+ const track = await getTrack(seq, "video", v);+ const clips = await trackClips(track);+ for (let i = 0; i < clips.length; i++) {+ if ((await clips[i].getStartTime()).ticks === t.ticks) {+ placed = await describeClip(clips[i], seq, "video", v, i, false);+ break;+ }+ }+ } catch (_) {}+ return { mode, item: item.name, at: await timeOut(t, seq), videoTrack: trackLabel("video", v), audioTrack: trackLabel("audio", au), placed };+}++async function clipStartingAt(track, t) {+ for (const c of await trackClips(track)) {+ if (Math.abs((await c.getStartTime()).ticksNumber - t.ticksNumber) < 1000) return c;+ }+ return null;+}++// ───────────────────────────── encoder events ─────────────────────────────++let encoderWatched = false;+function watchEncoder() {+ if (encoderWatched) return;+ encoderWatched = true;+ try {+ const em = ppro.EncoderManager.getManager();+ const E = ppro.EncoderManager;+ const on = (name, kind) =>+ ppro.EventManager.addEventListener(em, name, (ev) => send({ type: "event", kind, data: toJsonSafe(ev), at: Date.now() }));+ on(E.EVENT_RENDER_PROGRESS, "export_progress");+ on(E.EVENT_RENDER_COMPLETE, "export_complete");+ on(E.EVENT_RENDER_ERROR, "export_error");+ on(E.EVENT_RENDER_CANCEL, "export_cancel");+ on(E.EVENT_RENDER_QUEUE, "export_queued");+ } catch (e) {+ log("encoder events unavailable: " + e);+ }+}++function watchProject() {+ try {+ const PE = ppro.ProjectEvent;+ for (const [name, kind] of [[PE.EVENT_OPENED, "project_opened"], [PE.EVENT_ACTIVATED, "project_activated"], [PE.EVENT_DIRTY, "project_dirty"]]) {+ ppro.EventManager.addGlobalEventListener(name, async () => {+ send({ type: "event", kind, at: Date.now() });+ send({ type: "hello_update", fields: await helloInfo() });+ });+ }+ } catch (e) {+ log("project events unavailable: " + e);+ }+}++function toJsonSafe(v, depth = 0) {+ if (v === null || v === undefined) return v === undefined ? null : v;+ if (depth > 6) return "[…]";+ if (typeof v !== "object") return typeof v === "function" ? "[function]" : v;+ if (typeof v.ticks === "string" && typeof v.seconds === "number") return { seconds: v.seconds, ticks: v.ticks };+ if (Array.isArray(v)) return v.map((x) => toJsonSafe(x, depth + 1));+ const out = {};+ for (const k of Object.keys(v)) {+ try {+ out[k] = toJsonSafe(v[k], depth + 1);+ } catch (_) {}+ }+ if (!Object.keys(out).length && typeof v.toString === "function" && v.toString !== Object.prototype.toString) return v.toString();+ return out;+}++// Exposed to premiere_eval as `h`.+const helpers = { project, findSequence, findItem, findClip, getTrack, trackClips, toTick, timeOut, tx, componentsOf, findComponent, findParam, dbToLevel, levelToDb, allItems, parseTrack, handlers };++// ───────────────────────────── boot ─────────────────────────────++watchProject();+connect();
plugin/uxp/manifest.jsonadded+32@@ -0,0 +1,32 @@+{+ "manifestVersion": 5,+ "id": "inc.adom.premiere.bridge",+ "name": "Adom Bridge",+ "version": "0.1.0",+ "main": "index.html",+ "host": { "app": "premierepro", "minVersion": "25.6.0" },+ "entrypoints": [+ {+ "type": "panel",+ "id": "adomBridgePanel",+ "label": { "default": "Adom Bridge" },+ "minimumSize": { "width": 220, "height": 120 },+ "maximumSize": { "width": 2000, "height": 2000 },+ "preferredDockedSize": { "width": 260, "height": 160 },+ "preferredFloatingSize": { "width": 300, "height": 180 },+ "icons": [+ { "width": 23, "height": 23, "path": "icons/dark.png", "scale": [1, 2], "theme": ["darkest", "dark", "medium"], "species": ["generic"] },+ { "width": 23, "height": 23, "path": "icons/light.png", "scale": [1, 2], "theme": ["lightest", "light"], "species": ["generic"] }+ ]+ }+ ],+ "icons": [+ { "width": 23, "height": 23, "path": "icons/dark.png", "scale": [1, 2], "theme": ["darkest", "dark", "medium"], "species": ["pluginList"] },+ { "width": 23, "height": 23, "path": "icons/light.png", "scale": [1, 2], "theme": ["lightest", "light"], "species": ["pluginList"] }+ ],+ "requiredPermissions": {+ "network": { "domains": "all" },+ "localFileSystem": "fullAccess",+ "allowCodeGenerationFromStrings": true+ }+}
scripts/build-release.shadded+15@@ -0,0 +1,15 @@+#!/usr/bin/env bash+# Build the macOS release on the desktop: zip (premiere-bridge binary + bridge.json at the+# ROOT) and its manifest, in ~/adom-dev/premiere-bridge/dist/. Prints the manifest.+set -euo pipefail+cd "$(dirname "$0")/.."+python3 scripts/sync-verbs.py+VER=$(python3 -c 'import json; print(json.load(open("bridge.json"))["version"])')+TMPDIR="${TMPDIR:-/tmp}" scripts/dev-sync.sh --no-run | grep -E "error|warning|Finished|-->" || true+SLUG=premiere-bridge+ZIP="adom-bridge-premiere-macos-v$VER.zip"+URL="https://wiki.adom.inc/download/adom/$SLUG/$VER/$ZIP"+MURL="https://wiki.adom.inc/api/v1/pages/$SLUG/files/adom-bridge-premiere-manifest-macos.json"+CMD="cd ~/adom-dev/premiere-bridge && rm -rf dist && mkdir -p dist/stage && cp target/release/premiere-bridge bridge.json launcher.py dist/stage/ && cd dist/stage && zip -q -X ../$ZIP premiere-bridge bridge.json launcher.py && cd .. && rm -rf stage && SHA=\$(shasum -a 256 $ZIP | cut -d' ' -f1) && SIZE=\$(stat -f%z $ZIP) && printf '{\n \"manifest_version\": 1,\n \"name\": \"premiere\",\n \"version\": \"$VER\",\n \"url\": \"$URL\",\n \"sha256\": \"%s\",\n \"size\": %s,\n \"platform\": \"darwin-arm64\",\n \"verbPrefixes\": [\"premiere_\"],\n \"healthEndpoint\": \"/health\",\n \"updateManifestUrl\": \"$MURL\",\n \"released_at\": \"%s\"\n}\n' \$SHA \$SIZE \$(date -u +%Y-%m-%dT%H:%M:%SZ) > adom-bridge-premiere-manifest-macos.json && unzip -l $ZIP && cat adom-bridge-premiere-manifest-macos.json"+python3 -c 'import json,sys; print(json.dumps({"command":sys.argv[1],"reason":"Build the Premiere bridge release zip and manifest","timeoutSeconds":120}))' "$CMD" \+ | adom-bridge shell_execute "$(cat)" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("stdout","")); print(d.get("stderr",""))'
scripts/dev-sync.shadded+21@@ -0,0 +1,21 @@+#!/usr/bin/env bash+# Dev loop: ship this source tree to the desktop over adom-bridge, build it there with+# cargo, and (re)start a standalone test server on a scratch port.+# scripts/dev-sync.sh # build + restart test server on :47911+# scripts/dev-sync.sh --no-run # build only+set -euo pipefail+cd "$(dirname "$0")/.."+PORT="${PB_TEST_PORT:-47911}"+TMP="${TMPDIR:-/tmp}"+TGZ="$TMP/pb-src.tgz"+tar czf "$TGZ" -C .. premiere-bridge/Cargo.toml premiere-bridge/bridge.json premiere-bridge/launcher.py premiere-bridge/src premiere-bridge/plugin+adom-bridge send_files "{\"filePaths\":[\"$TGZ\"],\"reason\":\"premiere-bridge dev sync\"}" >/dev/null+RUN=""+if [[ "${1:-}" != "--no-run" ]]; then+ RUN="pkill -f 'premiere-bridge --port $PORT' || true; (nohup ./target/release/premiere-bridge --port $PORT > /tmp/pb-test.log 2>&1 &); sleep 1; curl -s localhost:$PORT/health"+fi+CMD="mkdir -p ~/adom-dev && cd ~/adom-dev && tar xzf ~/Downloads/pb-src.tgz && rm ~/Downloads/pb-src.tgz && cd premiere-bridge && ~/.cargo/bin/cargo build --release 2>&1 | grep -E '^(error|warning)|^ +-->|Finished|could not compile' ; $RUN"+python3 - "$CMD" <<'EOF' | adom-bridge shell_execute "$(cat)" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("stdout","")); print(d.get("stderr",""))'+import json, sys+print(json.dumps({"command": sys.argv[1], "reason": "Build and restart the Premiere bridge dev server", "timeoutSeconds": 580}))+EOF
scripts/pb.shadded+8@@ -0,0 +1,8 @@+#!/usr/bin/env bash+# Dev helper: pb.sh <verb> [json-args] — call the dev bridge server (:47911) on the desktop+# through adom-bridge shell_execute. Prints the verb's JSON response.+VERB="$1"; ARGS="${2:-{\}}"; PORT="${PB_TEST_PORT:-47911}"+BODY=$(python3 -c 'import json,sys; print(json.dumps({"command":sys.argv[1],"args":json.loads(sys.argv[2])}))' "$VERB" "$ARGS")+CMD=$(python3 -c 'import sys,shlex; print("curl -s -m 600 -X POST -H \"Content-Type: application/json\" --data-binary "+shlex.quote(sys.argv[1])+" localhost:"+sys.argv[2]+"/command")' "$BODY" "$PORT")+python3 -c 'import json,sys; print(json.dumps({"command":sys.argv[1],"reason":"Drive the Premiere bridge dev server","timeoutSeconds":600}))' "$CMD" \+ | adom-bridge shell_execute "$(cat)" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("stdout","") or json.dumps(d))'
scripts/seq.shadded+8@@ -0,0 +1,8 @@+#!/usr/bin/env bash+# Dev helper: compact dump of the active sequence's tracks/clips.+"$(dirname "$0")/pb.sh" premiere_get_sequence '{"detail":false}' | python3 -c '+import json,sys; d=json.load(sys.stdin)+if not d.get("success"): print(d.get("error")); sys.exit()+print(d["name"], d["width"],"x",d["height"], d["fps"],"fps dur", d["duration"]["seconds"])+for t in d["videoTracks"]+d["audioTracks"]:+ if t["clips"]: print(" ",t["track"], [(c["ref"],c["name"][:14],c["start"]["seconds"],c["end"]["seconds"],"in",c["inPoint"]) for c in t["clips"]])'
scripts/sync-verbs.pyadded+10@@ -0,0 +1,10 @@+#!/usr/bin/env python3+"""Regenerate bridge.json `verbs` from the catalog in src/catalog.rs."""+import json, pathlib, re+root = pathlib.Path(__file__).resolve().parent.parent+names = re.findall(r'^\s+v\("([a-z_0-9]+)",', (root / "src/catalog.rs").read_text(), re.M)+bj = root / "bridge.json"+b = json.loads(bj.read_text())+b["verbs"] = ["premiere_" + n for n in names]+bj.write_text(json.dumps(b, indent=2) + "\n")+print(f"{len(names)} verbs")
skills/premiere-bridge/SKILL.mdadded+71@@ -0,0 +1,71 @@+---+name: premiere-bridge+description: Drive Adobe Premiere Pro on the user's desktop through the Adom Bridge premiere bridge (premiere_* verbs) with no mouse or keyboard. Open or create projects, import media, build and edit timelines (insert, overwrite, append, move, trim, razor, ripple delete, close gaps), effects, transitions, keyframes, audio levels and fades, markers, transcripts, rendered preview frames and contact sheets of the cut, and exports to MP4/ProRes/etc. Trigger words: premiere, premiere pro, adobe premiere, edit my video, video timeline, add a clip, cut the clip, trim the clip, razor, ripple delete, add a transition, cross dissolve, fade the audio, lower the music, volume, add a marker, show me a frame, preview the edit, storyboard, contact sheet, export the video, render the sequence, premiere_get_sequence, premiere_preview_frame, premiere_razor.+user-invocable: true+---++Parent skill: **premiere-bridge** (this is the core skill; sub-skills: `premiere-editing`, `premiere-preview-export`)++# Premiere Pro bridge++The bridge is a Rust server Adom Bridge runs on the user's desktop, plus a UXP plugin **inside** Premiere that executes every verb against Adobe's `premierepro` API. Nothing clicks or types: it never touches the user's mouse, keyboard or focus. Call verbs with `adom-bridge premiere_<verb> '{...json args...}'`.++## FIRST-TIME / COLD-START: read before you panic++| You see | It means | Do this |+|---|---|---|+| `errorCode: plugin_not_connected`, `state: needs-plugin` | The in-app plugin isn't installed yet | `premiere_install_plugin` (no user clicks; installs through Adobe's own installer and Premiere hot-loads it), then `premiere_wait_for_plugin` |+| `state: app-not-running` | Premiere is closed | `premiere_launch` (optionally `{project:"/path/x.prproj"}`); it waits for the plugin |+| `state: waiting-for-plugin` | Premiere is starting up | `premiere_wait_for_plugin {timeoutSec:90}` |+| `state: no-app` | Premiere isn't installed | Offer to install it for the user through Creative Cloud. Never tell them to go download it themselves |+| `errorCode: cep_not_connected` | A helper-only verb (speed, add tracks, ExtendScript) needs the CEP helper, which loads at Premiere start | `premiere_install_plugin`, then ask the user before restarting Premiere (`premiere_quit {save:true}` then `premiere_launch`) |+| `errorCode: plugin_timeout` | Premiere is busy or a modal dialog is open | `premiere_status` (shows `responsive`); a `premiere_monitor_snapshot` shows any dialog |++For "is it ready?" questions call `premiere_readiness`. It is read-only and never launches or installs anything.++## The 8 rules++1. **Look before you edit.** `premiere_get_sequence` is the timeline view. Every clip has a `ref` like `"V1:0"` or `"A2:3"` (track : index in time order, 0-based). **Indexes shift after edits**, so re-read the sequence before the next edit.+2. **Linked A/V moves together.** `trim_clip`, `move_clip`, `remove_clips`, `remove_gap` and `razor` also apply to the clip's linked audio/video partner unless you pass `linked:false`.+3. **Times** are seconds (`12.5`), timecode (`"00:00:12;30"`; `;` = drop-frame at 29.97/59.94), frames (`"300f"`), or `{ticks}`. Responses give `{seconds, ticks, timecode, frame}`.+4. **`success:true` is not enough: look at the result.** After an edit, `premiere_preview_frame {time}` returns the rendered frame inline (base64 PNG). Look at it before telling the user it worked.+5. **Paths are on the desktop**, not in the container. Send media with `adom-bridge send_files` first, and `adom-bridge pull_file` exports back.+6. **Every edit is one undo step** in Premiere (Edit > Undo), labelled with the action. Tell the user they can undo.+7. **Don't disrupt the user.** Never quit or restart Premiere, close their project, or save over it without asking. Do experiments in a duplicated sequence (`premiere_duplicate_sequence`) or a scratch project.+8. **Relay `_hint` verbatim** when a verb fails, and branch on `errorCode`, not on the prose.++## Core verbs (the 80% path)++| Verb | What it does | Key args |+|---|---|---|+| `premiere_status` | Connection, Premiere version, open project | — |+| `premiere_get_project` | Project name/path, sequences, bins | — |+| `premiere_list_project_items` | Media in the Project panel with ids, paths, durations | `search`, `bin`, `type` |+| `premiere_import_media` | Import files | `paths:[…]`, `bin` |+| `premiere_create_sequence` | New sequence (matches clip settings) | `name`, `fromItems:[item]` |+| `premiere_set_active_sequence` | Switch timeline | `sequence` |+| `premiere_get_sequence` | Tracks + clips with refs, in/out, effects | `detail` |+| `premiere_append_clip` / `premiere_insert_clip` / `premiere_overwrite_clip` | Put a project item on the timeline | `item`, `time`, `track:"V1"`, `audioTrack` |+| `premiere_trim_clip` | Trim | `clip`, `start`/`end` or `trimStart`/`trimEnd` |+| `premiere_razor` | Cut at a time | `time`, `tracks?` |+| `premiere_remove_clips` | Delete / ripple delete | `clip(s)`, `ripple` |+| `premiere_move_clip` | Move | `clip`, `to` or `by` |+| `premiere_set_clip_volume` / `premiere_audio_fade` | Audio level in dB, fades | `clip:"A1:0"`, `db`, `type`, `duration` |+| `premiere_add_transition` | Cross Dissolve etc. | `clip`, `name`, `duration`, `position` |+| `premiere_preview_frame` | **See** a frame (inline PNG) | `time`, `width` |+| `premiere_contact_sheet` | Storyboard of the cut | `count`, `start`, `end` |+| `premiere_export_sequence` | Render to a file | `output`, `preset`, `range` |+| `premiere_eval` | Anything else: raw JS against the `premierepro` API | `code` |++Full catalog: `premiere_describe`. Recipes: the `premiere-editing` sub-skill (cutting, effects, audio, keyframes) and the `premiere-preview-export` sub-skill (seeing your work, exporting).++## A typical session++```bash+adom-bridge premiere_status+adom-bridge premiere_get_sequence '{"detail":false}'+adom-bridge premiere_razor '{"time":12.0}'+adom-bridge premiere_get_sequence '{"detail":false}' # find the new halves+adom-bridge premiere_remove_clips '{"clip":"V1:3","ripple":true}'+adom-bridge premiere_preview_frame '{"time":11.9}' # look at the result+```
skills/premiere-editing/SKILL.mdadded+78@@ -0,0 +1,78 @@+---+name: premiere-editing+description: Recipes for editing a Premiere Pro timeline through the premiere bridge — rough cuts from clips, razor + ripple delete, closing gaps, trimming, effects (Gaussian Blur, Lumetri, etc.), Motion/Opacity transforms, keyframe animation, video transitions, audio volume in dB, audio fades and ducking, markers, MOGRT titles, clip speed, and adding tracks. Trigger words: rough cut, cut out the silence, remove this part, tighten the edit, close the gap, add blur, zoom in, scale the clip, fade to black, fade in, cross dissolve, lower the volume, duck the music, fade out the audio, add a title, speed up the clip, slow motion, add a track, keyframe.+user-invocable: true+---++Parent skill: **premiere-bridge**++# Editing recipes++Re-read `premiere_get_sequence` between edits: clip refs (`"V1:2"`) are positional and shift.++## Build a rough cut+```bash+adom-bridge premiere_import_media '{"paths":["/Users/me/Footage/a.mp4","/Users/me/Footage/b.mp4"]}'+adom-bridge premiere_create_sequence '{"name":"Rough Cut","fromItems":["a.mp4"]}' # matches a.mp4's size/fps+adom-bridge premiere_append_clip '{"item":"b.mp4"}'+adom-bridge premiere_contact_sheet '{"count":8}'+```+`item` accepts the project item's id, exact name, `"Bin/name"`, or `{mediaPath}`.++## Remove a section (e.g. 12.0–15.5 s)+```bash+adom-bridge premiere_razor '{"time":12.0}'+adom-bridge premiere_razor '{"time":15.5}'+adom-bridge premiere_get_sequence '{"detail":false}' # the middle piece is the one starting at 12.0+adom-bridge premiere_remove_clips '{"clip":"V1:3","ripple":true}' # its linked audio goes too+```+`razor` uses the CEP helper's true razor when it's connected (`method:"qe-razor"`). Otherwise it uses an exact UXP fallback (`method:"uxp-overwrite"`), which keeps A/V sync but leaves effects on the left half only, and refuses clips with a speed change.++## Trim / move / gaps+- `premiere_trim_clip {clip, trimStart:0.5}` shaves 0.5 s off the head. `trimEnd` shaves the tail. `start`/`end` set absolute timeline times.+- `premiere_remove_gap {clip}` pulls the clip and everything after it left to close the gap before it.+- `premiere_move_clip {clip, by:-2}` or `{to:30}`. Moving onto another clip overwrites it.++## Effects and parameters+```bash+adom-bridge premiere_list_effects '{"kind":"video","search":"blur"}'+adom-bridge premiere_add_effect '{"clip":"V1:0","name":"Gaussian Blur"}'+adom-bridge premiere_get_clip_effects '{"clip":"V1:0"}' # exact component + param names, values+adom-bridge premiere_set_param '{"clip":"V1:0","component":"Gaussian Blur","param":"<name from get_clip_effects>","value":30}'+```+Always take `component`/`param` names from `get_clip_effects`; they vary by effect and Premiere version.+- **Motion > Position is normalized**: `[0.5,0.5]` is the frame centre. Scale is a percent.+- `premiere_set_transform {clip, scale:120, position:[0.45,0.5], rotation, opacity}` sets several at once.++## Keyframes (fades, zooms)+```bash+adom-bridge premiere_add_keyframes '{"clip":"V1:1","component":"Opacity","param":"Opacity","keyframes":[{"time":11,"value":0},{"time":13,"value":100}],"interpolation":"linear"}'+```+Times are **sequence time**; the bridge converts them to the clip's media time for you (pass `timeBase:"clip"` to give media time directly). Enabling animation also drops an automatic key at the clip start.++## Transitions+`premiere_add_transition {clip, name:"Cross Dissolve", duration:1, position:"end"|"start"}`. Video only. For audio, fade with keyframes.++## Audio+- Level: `premiere_set_clip_volume {clip:"A1:0", db:-6}`. 0 dB = unity. Add `time` to set a keyframe instead.+- Fades: `premiere_audio_fade {clip:"A2:0", type:"out", duration:2}`.+- Ducking music under a voiceover: put volume keyframes on the music clip around the VO's start/end with `premiere_set_clip_volume {clip, db:-18, time}`.+- `premiere_audio_probe {clip}` reads back the level in dB.+- Mute a track: `premiere_set_track_mute {track:"A3"}`.++## Markers, titles, transcripts+- `premiere_add_marker {time, name, comments, colorIndex}` and `premiere_list_markers`.+- Titles: `premiere_insert_mogrt {path:"…/Lower Third.mogrt", time, track:"V2"}`, then `get_clip_effects` + `set_param` to change the text.+- `premiere_transcribe {item}` starts Speech-to-Text. `premiere_get_transcript {item}` returns word timings, which are good for finding cut points.++## Helper-only verbs (need the CEP helper connected)+`premiere_set_speed {clip, speed:0.5, ripple}`, `premiere_add_tracks {video:1, audio:1}`, `premiere_remove_track`, `premiere_lock_track`, `premiere_set_track_target`, `premiere_extendscript {code}`. These act on the **active** sequence.++## Anything else+`premiere_eval {code}` runs an async JS function body inside Premiere with `ppro` (the API), `h` (helpers: `findClip`, `toTick`, `tx`, `trackClips`…) and `args`. Edits must go through `h.tx(project, "label", () => [actions])`, with the actions built synchronously:+```js+const p = await ppro.Project.getActiveProject(); const seq = await p.getActiveSequence();+const { clip } = await h.findClip(seq, "V1:0");+h.tx(p, "Disable", () => clip.createSetDisabledAction(true));+return "ok";+```
skills/premiere-preview-export/SKILL.mdadded+35@@ -0,0 +1,35 @@+---+name: premiere-preview-export+description: Seeing and delivering a Premiere Pro edit through the premiere bridge — render a frame of the sequence inline, a storyboard/contact sheet of the whole cut, a read-only snapshot of Premiere's Program Monitor, the Source Monitor, export presets (H.264/YouTube/ProRes), rendering the sequence or an in/out range to a file, OTIO/FCPXML/AAF interchange, and pulling the result back. Trigger words: show me the frame, what does it look like, preview, thumbnail, storyboard, contact sheet, screenshot premiere, export the video, render to mp4, export for youtube, prores, export xml, otio, pull the export.+user-invocable: true+---++Parent skill: **premiere-bridge**++# Seeing your work, and exporting it++## Look at the edit+| Need | Verb | Notes |+|---|---|---|+| One frame, inline | `premiere_preview_frame {time, width:960}` | Returns `image.base64` (PNG) + `savedTo`. Default time = playhead. |+| The whole cut at a glance | `premiere_contact_sheet {count:8, width:480}` | Evenly spaced frames, each inline. Keep `count` ≤ 16. |+| Exactly what Premiere shows | `premiere_monitor_snapshot {time?}` | Read-only capture of the Premiere window via Adom Bridge (no clicks, no focus change). |+| A file on disk | `premiere_export_frame {time, dir, format:"png"\|"jpg"}` | Defaults to `~/.adom/premiere-bridge/previews`. |++⚠ **Premiere's frame exporter doesn't evaluate keyframes between two keys.** A fade or zoom shows its before/after state only, and the static look (effects, grade, titles) is correct. To check a frame in the middle of an animation, use `premiere_monitor_snapshot {time}`.++Black frames usually mean a gap in the timeline or opacity at 0. Check `premiere_get_sequence`.++## Export+```bash+adom-bridge premiere_list_export_presets '{"search":"youtube"}' # name, format (H264, ProRes…), .epr path+adom-bridge premiere_set_in_out '{"in":0,"out":30}' # optional: just part of it+adom-bridge premiere_export_sequence '{"output":"/Users/me/Movies/cut.mp4","preset":"<.epr path>","range":"inout"}'+adom-bridge pull_file '{"filePaths":["/Users/me/Movies/cut.mp4"],"saveTo":"/tmp"}' # bring it to the container+```+- `mode:"immediate"` (default) renders inside Premiere and returns once the file is written. Long renders may need `timeoutSec`.+- `mode:"ame"` queues to Adobe Media Encoder. Watch `premiere_events` for `export_progress` / `export_complete`.+- Interchange: `premiere_export_interchange {output, format:"otio"|"fcpxml"|"aaf"}`.++## Source Monitor+`premiere_source_monitor {item, time}` opens a clip. `premiere_set_source_in_out {item, in, out}` marks the range that `insert_clip`/`overwrite_clip` will use.
src/ab.rsadded+63@@ -0,0 +1,63 @@+//! Calling Adom Bridge back over its loopback direct API (ADOM_DIRECT_API_URL, fallback+//! ~/.adom/direct-api-port). Used for things only Adom Bridge can do — e.g. a read-only+//! window capture of Premiere's Program Monitor. Minimal HTTP/1.1 client: one POST, JSON.++use serde_json::{json, Value};+use tokio::io::{AsyncReadExt, AsyncWriteExt};++fn base() -> Option<String> {+ if let Ok(u) = std::env::var("ADOM_DIRECT_API_URL") {+ return Some(u.trim_end_matches('/').to_string());+ }+ let p = crate::host::home_dir().join(".adom").join("direct-api-port");+ let s = std::fs::read_to_string(p).ok()?;+ let s = s.trim();+ Some(if s.contains(':') { format!("http://{s}") } else { format!("http://127.0.0.1:{s}") })+}++pub async fn command(verb: &str, args: Value) -> Result<Value, String> {+ let base = base().ok_or("Adom Bridge direct API not found (ADOM_DIRECT_API_URL unset)")?;+ let hostport = base.strip_prefix("http://").ok_or("direct API must be http://")?.to_string();+ let body = json!({"command": verb, "args": args}).to_string();+ let token = std::env::var("ADOM_BRIDGE_TOKEN").unwrap_or_default();+ let mut req = format!(+ "POST /command HTTP/1.1\r\nHost: {hostport}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",+ body.len()+ );+ if !token.is_empty() {+ req.push_str(&format!("X-Adom-Bridge-Token: {token}\r\n"));+ }+ req.push_str("\r\n");+ req.push_str(&body);+ let fut = async {+ let mut s = tokio::net::TcpStream::connect(&hostport).await.map_err(|e| e.to_string())?;+ s.write_all(req.as_bytes()).await.map_err(|e| e.to_string())?;+ let mut buf = Vec::new();+ s.read_to_end(&mut buf).await.map_err(|e| e.to_string())?;+ Ok::<Vec<u8>, String>(buf)+ };+ let buf = tokio::time::timeout(std::time::Duration::from_secs(60), fut).await.map_err(|_| "Adom Bridge timed out".to_string())??;+ let text = String::from_utf8_lossy(&buf);+ let (head, rest) = text.split_once("\r\n\r\n").ok_or("bad HTTP response")?;+ let body = if head.to_ascii_lowercase().contains("transfer-encoding: chunked") { dechunk(rest) } else { rest.to_string() };+ let mut v: Value = serde_json::from_str(&body).map_err(|e| format!("bad JSON from Adom Bridge: {e}"))?;+ // Adom Bridge returns the verb's own JSON as a string in `output`.+ if let Some(inner) = v["output"].as_str().and_then(|s| serde_json::from_str::<Value>(s).ok()) {+ v["output"] = inner;+ }+ Ok(v)+}++fn dechunk(s: &str) -> String {+ let mut out = String::new();+ let mut rest = s;+ while let Some((len, after)) = rest.split_once("\r\n") {+ let n = usize::from_str_radix(len.trim(), 16).unwrap_or(0);+ if n == 0 || after.len() < n {+ break;+ }+ out.push_str(&after[..n]);+ rest = after[n..].trim_start_matches("\r\n");+ }+ out+}
src/catalog.rsadded+276@@ -0,0 +1,276 @@+//! 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})+}
src/host.rsadded+671@@ -0,0 +1,671 @@+//! Everything that runs on the bridge side without the UXP plugin: finding,+//! launching and quitting Premiere, installing the plugin, connection state,+//! and the read-only status / readiness / describe verbs.++use crate::{catalog, plugin_pkg, Shared, PREFIX, PROTOCOL, VERSION};+use serde_json::{json, Value};+use std::{+ path::{Path, PathBuf},+ time::Duration,+};+use tokio::process::Command;++pub fn home_dir() -> PathBuf {+ std::env::var_os("HOME")+ .or_else(|| std::env::var_os("USERPROFILE"))+ .map(PathBuf::from)+ .unwrap_or_else(std::env::temp_dir)+}++/// User state lives under ~/.adom/premiere-bridge — never beside the executable+/// (on macOS that would break the Adom Bridge.app code seal).+pub fn data_dir() -> PathBuf {+ home_dir().join(".adom").join("premiere-bridge")+}++pub fn load_or_create_token(dir: &Path) -> String {+ let path = dir.join("token");+ if let Ok(t) = std::fs::read_to_string(&path) {+ let t = t.trim().to_string();+ if t.len() >= 32 {+ return t;+ }+ }+ use rand::Rng;+ let token: String = rand::thread_rng()+ .sample_iter(&rand::distributions::Alphanumeric)+ .take(48)+ .map(char::from)+ .collect();+ let _ = std::fs::write(&path, &token);+ restrict_permissions(&path);+ token+}++#[cfg(unix)]+fn restrict_permissions(path: &Path) {+ use std::os::unix::fs::PermissionsExt;+ let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));+}+#[cfg(not(unix))]+fn restrict_permissions(_: &Path) {}++/// The UXP plugin reads this to find us. It re-reads it on every reconnect attempt,+/// so a port change after an Adom Bridge restart heals by itself.+pub fn write_connection_file(st: &Shared) {+ let path = st.data_dir.join("connection.json");+ let body = json!({+ "port": st.port,+ "token": st.token,+ "url": format!("ws://127.0.0.1:{}/ws?token={}", st.port, st.token),+ "bridgeVersion": VERSION,+ "protocol": PROTOCOL,+ "pid": std::process::id(),+ });+ let _ = std::fs::write(&path, serde_json::to_vec_pretty(&body).unwrap_or_default());+ restrict_permissions(&path);+}++// ───────────────────────────── finding Premiere ─────────────────────────────++#[derive(Clone, Debug)]+pub struct Install {+ pub year: u32,+ pub app: PathBuf,+}++/// Every Premiere Pro install on this machine, newest first.+pub fn find_installs() -> Vec<Install> {+ let mut out = Vec::new();+ let roots: Vec<PathBuf> = if cfg!(target_os = "macos") {+ vec![PathBuf::from("/Applications")]+ } else if cfg!(windows) {+ let pf = std::env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".into());+ vec![PathBuf::from(pf).join("Adobe")]+ } else {+ vec![]+ };+ for root in roots {+ let Ok(rd) = std::fs::read_dir(&root) else { continue };+ for e in rd.flatten() {+ let name = e.file_name().to_string_lossy().into_owned();+ let Some(rest) = name.strip_prefix("Adobe Premiere Pro") else { continue };+ let year: u32 = rest.trim().chars().filter(|c| c.is_ascii_digit()).collect::<String>().parse().unwrap_or(0);+ let app = if cfg!(target_os = "macos") {+ e.path().join(format!("{name}.app"))+ } else {+ e.path().join("Adobe Premiere Pro.exe")+ };+ if app.exists() {+ out.push(Install { year, app });+ }+ }+ }+ out.sort_by(|a, b| b.year.cmp(&a.year));+ out+}++/// Main Premiere executables currently running (not crashpad / IPC helpers that+/// merely carry the app path in their argv).+pub async fn running_premiere() -> Vec<String> {+ if cfg!(windows) {+ let out = Command::new("tasklist")+ .args(["/FO", "CSV", "/NH", "/FI", "IMAGENAME eq Adobe Premiere Pro.exe"])+ .output()+ .await;+ return match out {+ Ok(o) if String::from_utf8_lossy(&o.stdout).contains("Adobe Premiere Pro.exe") => {+ vec!["Adobe Premiere Pro.exe".into()]+ }+ _ => vec![],+ };+ }+ let Ok(o) = Command::new("ps").args(["-axo", "comm="]).output().await else { return vec![] };+ String::from_utf8_lossy(&o.stdout)+ .lines()+ .filter(|l| {+ let base = l.rsplit('/').next().unwrap_or("");+ base.starts_with("Adobe Premiere Pro") && l.contains("/Contents/MacOS/")+ })+ .map(str::to_string)+ .collect()+}++pub async fn premiere_running() -> bool {+ !running_premiere().await.is_empty()+}++// ───────────────────────────── local verbs ─────────────────────────────++/// Verbs answered here. `None` means "forward to the plugin".+pub async fn local_verb(st: &Shared, bare: &str, args: &Value) -> Option<Value> {+ Some(match bare {+ "describe" => catalog::describe(),+ "status" => status(st).await,+ "readiness" => readiness(st).await,+ "launch" => launch(st, args).await,+ "quit" => quit(st, args).await,+ "install_plugin" => plugin_pkg::install(st, args).await,+ "plugin_info" => plugin_pkg::info(st).await,+ "events" => {+ let since = args["sinceSeq"].as_u64().unwrap_or(0);+ let (events, last) = st.recent_events(since).await;+ json!({"success": true, "events": events, "lastSeq": last})+ }+ "wait_for_plugin" => wait_for_plugin(st, args).await,+ "read_file_base64" => read_file_base64(args).await,+ "preview_frame" => preview_frame(st, args).await,+ "contact_sheet" => contact_sheet(st, args).await,+ "list_export_presets" => list_export_presets(args).await,+ "monitor_snapshot" => monitor_snapshot(st, args).await,+ _ => return None,+ })+}++pub async fn status(st: &Shared) -> Value {+ let running = running_premiere().await;+ let installs: Vec<Value> = find_installs()+ .iter()+ .map(|i| json!({"year": i.year, "app": i.app}))+ .collect();+ let hello = st.plugin_hello().await;+ let cep = st.runtime_hello("cep").await;+ let mut out = json!({+ "success": true,+ "cepHelperConnected": cep.is_some(),+ "cepHelper": cep.map(|(h, _)| h),+ "bridgeVersion": VERSION,+ "port": st.port,+ "premiereInstalled": !installs.is_empty(),+ "installs": installs,+ "premiereRunning": !running.is_empty(),+ "runningProcesses": running,+ "pluginConnected": hello.is_some(),+ "pluginInstalled": plugin_pkg::installed_version().is_some(),+ "pluginInstalledVersion": plugin_pkg::installed_version(),+ "pluginBundledVersion": plugin_pkg::BUNDLED_VERSION,+ "dataDir": st.data_dir,+ });+ if let Some((h, secs)) = hello {+ out["plugin"] = h;+ out["pluginConnectedForSec"] = json!(secs);+ // Ask the plugin for a live snapshot; a hung Premiere (modal dialog) shows up here.+ match st.call_plugin("ping", json!({}), Duration::from_secs(5)).await {+ Ok(r) => {+ out["responsive"] = json!(true);+ out["live"] = r.get("result").cloned().unwrap_or(Value::Null);+ }+ Err(_) => out["responsive"] = json!(false),+ }+ }+ out+}++/// Read-only, never launches or installs anything.+pub async fn readiness(st: &Shared) -> Value {+ let installs = find_installs();+ let running = premiere_running().await;+ let connected = st.plugin_connected().await;+ let installed = plugin_pkg::installed_version();+ let state = if installs.is_empty() {+ "no-app"+ } else if connected {+ "ready"+ } else if installed.is_none() {+ "needs-plugin"+ } else if !running {+ "app-not-running"+ } else {+ "waiting-for-plugin"+ };+ json!({+ "success": true,+ "ready": connected,+ "state": state,+ "installing": false,+ "premiereInstalled": !installs.is_empty(),+ "premiereRunning": running,+ "pluginInstalled": installed.is_some(),+ "pluginConnected": connected,+ "lastError": Value::Null,+ })+}++pub async fn not_connected_response(st: &Shared) -> Value {+ let r = readiness(st).await;+ let state = r["state"].as_str().unwrap_or("");+ let hint = match state {+ "no-app" => "Premiere Pro isn't installed on this machine. Offer to install it for the user through Creative Cloud (Adom Bridge can open the Creative Cloud app); never tell them to go download it themselves.",+ "needs-plugin" => "The Adom UXP plugin isn't installed in Premiere yet. Call premiere_install_plugin (no user clicks needed), then premiere_wait_for_plugin.",+ "app-not-running" => "Premiere Pro isn't running. Call premiere_launch (optionally with {project:\"/path/file.prproj\"}), then premiere_wait_for_plugin.",+ _ => "Premiere is running and the plugin is installed but hasn't connected yet. Call premiere_wait_for_plugin {timeoutSec:60}. If it never connects, the plugin panel may need opening once: Window > UXP Plugins > Adom Bridge (after that it auto-starts).",+ };+ json!({+ "success": false,+ "errorCode": "plugin_not_connected",+ "state": state,+ "error": "The Adom plugin inside Premiere Pro is not connected, so this verb can't run yet.",+ "_hint": hint,+ "_next": match state {+ "needs-plugin" => json!(["premiere_install_plugin", "premiere_wait_for_plugin"]),+ "app-not-running" => json!(["premiere_launch", "premiere_wait_for_plugin"]),+ _ => json!(["premiere_wait_for_plugin", "premiere_status"]),+ },+ })+}++async fn wait_for_plugin(st: &Shared, args: &Value) -> Value {+ let timeout = args["timeoutSec"].as_u64().unwrap_or(90).clamp(1, 600);+ let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout);+ loop {+ if let Some((hello, _)) = st.plugin_hello().await {+ return json!({"success": true, "connected": true, "plugin": hello});+ }+ if tokio::time::Instant::now() >= deadline {+ let mut r = not_connected_response(st).await;+ r["error"] = json!(format!("plugin did not connect within {timeout}s"));+ return r;+ }+ tokio::time::sleep(Duration::from_millis(500)).await;+ }+}++async fn launch(st: &Shared, args: &Value) -> Value {+ let installs = find_installs();+ let want_year = args["year"].as_u64().map(|y| y as u32);+ let Some(inst) = installs+ .iter()+ .find(|i| want_year.map_or(true, |y| i.year == y))+ .cloned()+ else {+ return json!({+ "success": false,+ "errorCode": "premiere_not_installed",+ "error": "No Adobe Premiere Pro install found.",+ "installs": installs.iter().map(|i| json!({"year": i.year, "app": i.app})).collect::<Vec<_>>(),+ });+ };+ let project = args["project"].as_str();+ if premiere_running().await {+ if let Some(p) = project {+ if st.plugin_connected().await {+ return crate::dispatch(st, "open_project", json!({"path": p})).await;+ }+ }+ return json!({"success": true, "alreadyRunning": true, "app": inst.app});+ }+ let mut cmd = if cfg!(target_os = "macos") {+ let mut c = Command::new("open");+ // -g: launch without stealing focus unless the caller wants Premiere in front.+ if !args["foreground"].as_bool().unwrap_or(true) {+ c.arg("-g");+ }+ c.arg("-a").arg(&inst.app);+ if let Some(p) = project {+ c.arg(p);+ }+ c+ } else {+ let mut c = Command::new(&inst.app);+ if let Some(p) = project {+ c.arg(p);+ }+ c+ };+ match cmd.spawn() {+ Ok(_) => {+ let mut out = json!({"success": true, "launched": true, "app": inst.app, "year": inst.year});+ if args["wait"].as_bool().unwrap_or(true) {+ let w = wait_for_plugin(st, &json!({"timeoutSec": args["timeoutSec"].as_u64().unwrap_or(120)})).await;+ out["pluginConnected"] = w["connected"].clone();+ if w["connected"].as_bool() != Some(true) {+ out["_hint"] = w["_hint"].clone();+ }+ }+ out+ }+ Err(e) => json!({"success": false, "errorCode": "launch_failed", "error": e.to_string()}),+ }+}++async fn quit(st: &Shared, args: &Value) -> Value {+ if !premiere_running().await {+ return json!({"success": true, "wasRunning": false});+ }+ if args["save"].as_bool().unwrap_or(false) && st.plugin_connected().await {+ let _ = st.call_plugin("save_project", json!({}), Duration::from_secs(60)).await;+ }+ let res = if cfg!(target_os = "macos") {+ let mut names: Vec<String> = running_premiere()+ .await+ .iter()+ .filter_map(|p| p.rsplit('/').next().map(str::to_string))+ .collect();+ names.dedup();+ let script = names+ .iter()+ .map(|n| format!("tell application \"{n}\" to quit"))+ .collect::<Vec<_>>()+ .join("\n");+ Command::new("osascript").arg("-e").arg(script).output().await+ } else {+ let mut c = Command::new("taskkill");+ c.args(["/IM", "Adobe Premiere Pro.exe"]);+ if args["force"].as_bool().unwrap_or(false) {+ c.arg("/F");+ }+ c.output().await+ };+ match res {+ Ok(o) if o.status.success() => json!({"success": true, "quitRequested": true}),+ Ok(o) => json!({"success": false, "errorCode": "quit_failed", "error": String::from_utf8_lossy(&o.stderr)}),+ Err(e) => json!({"success": false, "errorCode": "quit_failed", "error": e.to_string()}),+ }+}++/// Inline a file the plugin wrote (preview frame, thumbnail) so the AI can look at it+/// without a separate pull_file round-trip. Capped to keep responses sane.+async fn read_file_base64(args: &Value) -> Value {+ use base64::Engine;+ let Some(path) = args["path"].as_str() else {+ return json!({"success": false, "errorCode": "bad_args", "error": "path is required"});+ };+ let max = args["maxBytes"].as_u64().unwrap_or(8 * 1024 * 1024);+ wait_stable(path, Duration::from_secs(10)).await;+ match tokio::fs::read(path).await {+ Ok(bytes) if (bytes.len() as u64) <= max => json!({+ "success": true,+ "path": path,+ "bytes": bytes.len(),+ "base64": base64::engine::general_purpose::STANDARD.encode(&bytes),+ }),+ Ok(bytes) => json!({+ "success": false,+ "errorCode": "too_large",+ "error": format!("{} bytes > maxBytes {max}", bytes.len()),+ "_hint": "Use adom-bridge pull_file to fetch big files into the container.",+ }),+ Err(e) => json!({"success": false, "errorCode": "read_failed", "error": e.to_string()}),+ }+}++#[allow(dead_code)]+pub fn prefixed(bare: &str) -> String {+ format!("{PREFIX}{bare}")+}++pub async fn cep_not_connected_response(st: &Shared) -> Value {+ let uxp = st.plugin_connected().await;+ json!({+ "success": false,+ "errorCode": "cep_not_connected",+ "error": "This verb runs in the Adom CEP helper (ExtendScript/QE), which isn't connected.",+ "_hint": if uxp {+ "The UXP plugin is up but the CEP helper isn't. Run premiere_install_plugin (installs the helper + enables unsigned CEP), then restart Premiere (premiere_quit {save:true}, premiere_launch). It starts invisibly with Premiere."+ } else {+ "Neither in-app plugin is connected. premiere_readiness says what's missing."+ },+ "_next": ["premiere_install_plugin", "premiere_quit", "premiere_launch"],+ })+}++pub fn previews_dir(st: &Shared) -> PathBuf {+ let d = st.data_dir.join("previews");+ let _ = std::fs::create_dir_all(&d);+ d+}++/// Bridge-side defaults so the AI rarely has to invent desktop paths.+pub fn fill_defaults(st: &Shared, bare: &str, mut args: Value) -> Value {+ if !args.is_object() {+ args = json!({});+ }+ match bare {+ "export_frame" | "export_frames" if args.get("dir").is_none() => {+ args["dir"] = json!(previews_dir(st));+ }+ "export_frame_qe" if args.get("path").is_none() => {+ let t = args["time"].as_f64().unwrap_or(0.0);+ args["path"] = json!(previews_dir(st).join(format!("qe_{}ms.png", (t * 1000.0) as i64)));+ }+ _ => {}+ }+ args+}++/// Premiere writes exported frames asynchronously after exportSequenceFrame resolves:+/// wait until the file exists and its size holds still before reading it.+async fn wait_stable(path: &str, timeout: Duration) -> bool {+ let deadline = tokio::time::Instant::now() + timeout;+ let mut last: Option<u64> = None;+ loop {+ let size = tokio::fs::metadata(path).await.map(|m| m.len()).ok();+ if let (Some(a), Some(b)) = (last, size) {+ if a == b && b > 0 {+ return true;+ }+ }+ last = size;+ if tokio::time::Instant::now() >= deadline {+ return size.unwrap_or(0) > 0;+ }+ tokio::time::sleep(Duration::from_millis(300)).await;+ }+}++fn encode_file(path: &str) -> Option<(String, usize)> {+ use base64::Engine;+ let bytes = std::fs::read(path).ok()?;+ Some((base64::engine::general_purpose::STANDARD.encode(&bytes), bytes.len()))+}++async fn preview_frame(st: &Shared, args: &Value) -> Value {+ let mut a = args.clone();+ if a.get("width").is_none() {+ a["width"] = json!(960);+ }+ if a.get("name").is_none() {+ a["name"] = json!(format!("preview_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0)));+ }+ let mut r = crate::dispatch(st, "export_frame", a).await;+ if r["success"].as_bool() != Some(true) {+ return r;+ }+ let path = r["path"].as_str().unwrap_or("").to_string();+ wait_stable(&path, Duration::from_secs(20)).await;+ if args["inline"].as_bool().unwrap_or(true) {+ match encode_file(&path) {+ Some((b64, n)) => {+ r["image"] = json!({"mime": "image/png", "base64": b64, "bytes": n});+ }+ None => {+ r["_hint"] = json!(format!("Frame export reported success but {path} isn't readable yet; retry premiere_read_file_base64 {{path}} in a second."));+ }+ }+ }+ r["savedTo"] = json!(path);+ r+}++async fn contact_sheet(st: &Shared, args: &Value) -> Value {+ let mut a = args.clone();+ if a.get("width").is_none() {+ a["width"] = json!(480);+ }+ if a.get("prefix").is_none() {+ a["prefix"] = json!(format!("sheet_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)));+ }+ let mut r = crate::dispatch(st, "export_frames", a).await;+ if r["success"].as_bool() != Some(true) {+ return r;+ }+ if args["inline"].as_bool().unwrap_or(true) {+ if let Some(frames) = r["frames"].as_array_mut() {+ for f in frames.iter_mut() {+ let p = f["path"].as_str().unwrap_or("").to_string();+ wait_stable(&p, Duration::from_secs(20)).await;+ if let Some((b64, n)) = encode_file(&p) {+ f["image"] = json!({"mime": "image/png", "base64": b64, "bytes": n});+ }+ }+ }+ }+ r+}++/// Walk the usual preset roots for .epr files. Depth-limited; fast enough to run per call.+async fn list_export_presets(args: &Value) -> Value {+ let search = args["search"].as_str().map(|s| s.to_lowercase());+ let home = home_dir();+ let mut roots: Vec<PathBuf> = vec![+ home.join("Documents/Adobe/Adobe Media Encoder"),+ home.join("Documents/Adobe/Premiere Pro"),+ ];+ for inst in find_installs() {+ if cfg!(target_os = "macos") {+ roots.push(inst.app.join("Contents/MediaIO/systempresets"));+ roots.push(inst.app.join("Contents/Settings/IngestPresets"));+ } else if let Some(dir) = inst.app.parent() {+ roots.push(dir.join("MediaIO").join("systempresets"));+ }+ }+ if cfg!(target_os = "macos") {+ if let Ok(rd) = std::fs::read_dir("/Applications") {+ for e in rd.flatten() {+ let n = e.file_name().to_string_lossy().into_owned();+ if n.starts_with("Adobe Media Encoder") {+ roots.push(e.path().join(format!("{n}.app/Contents/MediaIO/systempresets")));+ }+ }+ }+ }+ let mut found = Vec::new();+ for r in &roots {+ walk_epr(r, 0, &mut found);+ }+ let mut presets: Vec<Value> = found+ .into_iter()+ .filter_map(|p| {+ let raw = read_preset_name(&p).unwrap_or_else(|| p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default());+ let name = clean_preset_name(&raw, &p);+ let format = preset_format(&p);+ let hay = format!("{} {} {}", name, format.as_deref().unwrap_or(""), p.display()).to_lowercase().replace('.', "");+ if search.as_ref().map_or(true, |q| hay.contains(&q.replace('.', ""))) {+ Some(json!({"name": name, "format": format, "path": p}))+ } else {+ None+ }+ })+ .collect();+ presets.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));+ // Premiere and Media Encoder ship identical copies; keep the first of each name+format.+ presets.dedup_by(|a, b| a["name"] == b["name"] && a["format"] == b["format"]);+ let total = presets.len();+ presets.truncate(args["limit"].as_u64().unwrap_or(80) as usize);+ json!({"success": true, "count": total, "presets": presets, "searchedRoots": roots})+}++fn walk_epr(dir: &Path, depth: usize, out: &mut Vec<PathBuf>) {+ if depth > 6 || out.len() > 5000 {+ return;+ }+ let Ok(rd) = std::fs::read_dir(dir) else { return };+ for e in rd.flatten() {+ let p = e.path();+ if p.is_dir() {+ walk_epr(&p, depth + 1, out);+ } else if p.extension().map_or(false, |x| x.eq_ignore_ascii_case("epr")) {+ out.push(p);+ }+ }+}++/// .epr files are XML with a <PresetName> or <Name> early on.+fn read_preset_name(p: &Path) -> Option<String> {+ use std::io::Read;+ let mut buf = vec![0u8; 16 * 1024];+ let n = std::fs::File::open(p).ok()?.read(&mut buf).ok()?;+ let s = String::from_utf8_lossy(&buf[..n]);+ for tag in ["PresetName", "Name"] {+ let open = format!("<{tag}>");+ if let Some(i) = s.find(&open) {+ let rest = &s[i + open.len()..];+ if let Some(j) = rest.find('<') {+ let v = rest[..j].trim();+ if !v.is_empty() {+ return Some(v.to_string());+ }+ }+ }+ }+ None+}++/// Read-only capture of Premiere's main window via Adom Bridge (no focus change, no+/// clicks). Shows exactly what the Program Monitor renders — including animated values+/// that Exporter.exportSequenceFrame doesn't evaluate.+async fn monitor_snapshot(st: &Shared, args: &Value) -> Value {+ if let Some(t) = args.get("time") {+ let r = crate::dispatch(st, "set_playhead", json!({"time": t, "sequence": args["sequence"]})).await;+ if r["success"].as_bool() != Some(true) {+ return r;+ }+ tokio::time::sleep(Duration::from_millis(args["settleMs"].as_u64().unwrap_or(700))).await;+ }+ let list = match crate::ab::command("desktop_list_windows", json!({})).await {+ Ok(v) => v,+ Err(e) => return json!({"success": false, "errorCode": "ab_unreachable", "error": e,+ "_hint": "This verb needs Adom Bridge's direct API (it's set when Adom Bridge spawns the bridge). Use premiere_preview_frame instead."}),+ };+ let wins = list["output"]["data"]["windows"].as_array().cloned().unwrap_or_default();+ let win = wins+ .iter()+ .filter(|w| {+ w["appBundleId"].as_str().map_or(false, |b| b.starts_with("com.adobe.PremierePro"))+ || w["title"].as_str().map_or(false, |t| t.contains(".prproj") || t.starts_with("Adobe Premiere Pro"))+ })+ .max_by_key(|w| w["rect"]["width"].as_i64().unwrap_or(0) * w["rect"]["height"].as_i64().unwrap_or(0));+ let Some(win) = win else {+ return json!({"success": false, "errorCode": "window_not_found", "error": "Couldn't find Premiere's main window.", "_hint": "Is Premiere running and not minimized? premiere_status."});+ };+ let shot = match crate::ab::command("desktop_screenshot_window", json!({"hwnd": win["hwnd"]})).await {+ Ok(v) => v,+ Err(e) => return json!({"success": false, "errorCode": "screenshot_failed", "error": e}),+ };+ let out = &shot["output"];+ let image = out["data"]["image"].as_str().or(out["image"].as_str()).map(str::to_string);+ json!({+ "success": image.is_some() || out["data"]["safePath"].is_string(),+ "window": {"hwnd": win["hwnd"], "title": win["title"]},+ "image": image.map(|b| json!({"mime": "image/png", "base64": b})),+ "safePath": out["data"]["safePath"].clone(),+ "fullPath": out["data"]["fullPath"].clone(),+ "error": if out.is_null() { shot["error"].clone() } else { Value::Null },+ })+}++/// "($$$/AME/.../PresetName=H.264 MP4 Proxy)" -> "H.264 MP4 Proxy"; else the file stem.+fn clean_preset_name(raw: &str, p: &Path) -> String {+ if let Some(i) = raw.find("PresetName=") {+ let v = &raw[i + 11..];+ let v = if raw.starts_with('(') { v.strip_suffix(')').unwrap_or(v) } else { v };+ return v.trim().to_string();+ }+ if raw.starts_with("$$$") || raw.starts_with("($$$") {+ return p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();+ }+ raw.to_string()+}++/// systempresets/<vendor-hex>_<format-hex> — e.g. 4E49434B_48323634 = "NICK"_"H264".+fn preset_format(p: &Path) -> Option<String> {+ let dir = p.parent()?.file_name()?.to_string_lossy().into_owned();+ let (_, fmt) = dir.split_once('_')?;+ if fmt.len() != 8 {+ return None;+ }+ let bytes: Option<Vec<u8>> = (0..4).map(|i| u8::from_str_radix(&fmt[i * 2..i * 2 + 2], 16).ok()).collect();+ let s: String = bytes?.into_iter().map(char::from).collect();+ let s = s.trim().to_string();+ (!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic() || c == ' ')).then_some(s)+}
src/main.rsadded+479@@ -0,0 +1,479 @@+//! Adom Bridge bridge for Adobe Premiere Pro.+//!+//! Two halves:+//! * this Rust server — Adom Bridge spawns it, POSTs `{command, args}` verbs at it,+//! and polls `/health` for the status chip;+//! * a UXP plugin running INSIDE Premiere (see `plugin/`) — it dials back into this+//! server over a loopback WebSocket (`/ws`) and executes verbs against the+//! `premierepro` scripting API. No mouse, no keyboard, no window automation.+//!+//! A verb arrives here, gets forwarded to the plugin as `{type:"call", id, verb, args}`,+//! and the plugin's `{type:"result", id, ok, result|error}` is wrapped with the+//! `_hint` / `_next` / `related` / `pitfalls` the calling AI actually reads.++mod ab;+mod catalog;+mod host;+mod plugin_pkg;++use axum::{+ body::Bytes,+ extract::{+ ws::{Message, WebSocket, WebSocketUpgrade},+ Query, State,+ },+ http::{header, HeaderMap, Method, StatusCode},+ response::{IntoResponse, Response},+ routing::{any, get},+ Json, Router,+};+use futures_util::{SinkExt, StreamExt};+use serde_json::{json, Value};+use std::{+ collections::HashMap,+ path::PathBuf,+ sync::{+ atomic::{AtomicU64, Ordering},+ Arc,+ },+ time::{Duration, Instant},+};+use tokio::sync::{mpsc, oneshot, Mutex};++pub const VERSION: &str = env!("CARGO_PKG_VERSION");+pub const PREFIX: &str = "premiere_";++/// Protocol version spoken between this server and the UXP plugin. Bump on any+/// breaking change to the call/result envelope; the plugin reports its own in `hello`.+pub const PROTOCOL: u64 = 1;++pub struct PluginConn {+ runtime: String,+ tx: mpsc::UnboundedSender<String>,+ hello: Value,+ connected_at: Instant,+ conn_id: u64,+}++pub struct AppState {+ pub port: u16,+ pub token: String,+ pub data_dir: PathBuf,+ pub started: Instant,+ /// Live in-app connections keyed by runtime: "uxp" (primary) and "cep" (helper).+ plugins: Mutex<HashMap<String, PluginConn>>,+ /// In-flight calls: id -> (connection that owns it, waiter).+ pending: Mutex<HashMap<u64, (u64, oneshot::Sender<Value>)>>,+ next_id: AtomicU64,+ /// Plugin-pushed events (selection changed, export finished...), newest last.+ events: Mutex<Vec<Value>>,+ event_seq: AtomicU64,+}++pub type Shared = Arc<AppState>;++impl AppState {+ pub async fn runtime_connected(&self, runtime: &str) -> bool {+ self.plugins.lock().await.contains_key(runtime)+ }++ /// The primary (UXP) plugin; "connected" in user-facing terms means this one.+ pub async fn plugin_connected(&self) -> bool {+ self.runtime_connected("uxp").await+ }++ pub async fn plugin_hello(&self) -> Option<(Value, u64)> {+ self.runtime_hello("uxp").await+ }++ pub async fn runtime_hello(&self, runtime: &str) -> Option<(Value, u64)> {+ self.plugins+ .lock()+ .await+ .get(runtime)+ .map(|p| (p.hello.clone(), p.connected_at.elapsed().as_secs()))+ }++ /// Send one verb to an in-app runtime ("uxp" or "cep") and wait for its result.+ pub async fn call_plugin(&self, verb: &str, args: Value, timeout: Duration) -> Result<Value, CallError> {+ self.call_runtime("uxp", verb, args, timeout).await+ }++ pub async fn call_runtime(&self, runtime: &str, verb: &str, args: Value, timeout: Duration) -> Result<Value, CallError> {+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);+ let (tx, rx) = oneshot::channel();+ {+ let guard = self.plugins.lock().await;+ let Some(conn) = guard.get(runtime) else {+ return Err(CallError::NotConnected);+ };+ self.pending.lock().await.insert(id, (conn.conn_id, tx));+ let msg = json!({"type": "call", "id": id, "verb": verb, "args": args}).to_string();+ if conn.tx.send(msg).is_err() {+ self.pending.lock().await.remove(&id);+ return Err(CallError::NotConnected);+ }+ }+ match tokio::time::timeout(timeout, rx).await {+ Ok(Ok(v)) => Ok(v),+ Ok(Err(_)) => Err(CallError::Disconnected),+ Err(_) => {+ self.pending.lock().await.remove(&id);+ Err(CallError::Timeout(timeout.as_secs()))+ }+ }+ }++ pub async fn recent_events(&self, since: u64) -> (Vec<Value>, u64) {+ let ev = self.events.lock().await;+ let out = ev+ .iter()+ .filter(|e| e["seq"].as_u64().unwrap_or(0) > since)+ .cloned()+ .collect();+ (out, self.event_seq.load(Ordering::Relaxed))+ }+}++#[derive(Debug)]+pub enum CallError {+ NotConnected,+ Disconnected,+ Timeout(u64),+}++// ───────────────────────────── entry ─────────────────────────────++#[tokio::main]+async fn main() {+ let mut port: u16 = 0;+ let mut args = std::env::args().skip(1);+ while let Some(a) = args.next() {+ if a == "--port" {+ port = args.next().and_then(|p| p.parse().ok()).unwrap_or(0);+ } else if let Some(p) = a.strip_prefix("--port=") {+ port = p.parse().unwrap_or(0);+ } else if a == "--version" {+ println!("premiere-bridge {VERSION}");+ return;+ }+ }+ let bind_host = std::env::var("ADOM_BIND_HOST").unwrap_or_else(|_| "127.0.0.1".into());+ let listener = tokio::net::TcpListener::bind((bind_host.as_str(), port))+ .await+ .unwrap_or_else(|e| panic!("bind {bind_host}:{port}: {e}"));+ let port = listener.local_addr().map(|a| a.port()).unwrap_or(port);++ let data_dir = host::data_dir();+ let _ = std::fs::create_dir_all(&data_dir);+ let token = host::load_or_create_token(&data_dir);++ let state: Shared = Arc::new(AppState {+ port,+ token,+ data_dir,+ started: Instant::now(),+ plugins: Mutex::new(HashMap::new()),+ pending: Mutex::new(HashMap::new()),+ next_id: AtomicU64::new(1),+ events: Mutex::new(Vec::new()),+ event_seq: AtomicU64::new(0),+ });++ // The plugin finds us by reading this file (port changes are picked up on its next+ // reconnect attempt), so write it before accepting anything.+ host::write_connection_file(&state);++ let app = Router::new()+ .route("/health", get(health))+ .route("/ws", get(ws_upgrade))+ .route("/", any(command))+ .route("/command", any(command))+ .with_state(state.clone());++ eprintln!("[premiere-bridge {VERSION}] listening on {bind_host}:{port} data={}", state.data_dir.display());+ axum::serve(listener, app).await.expect("server");+}++// ───────────────────────────── /health ─────────────────────────────++async fn health(State(st): State<Shared>) -> Json<Value> {+ let running = host::premiere_running().await;+ let hello = st.plugin_hello().await;+ let cep = st.runtime_connected("cep").await;+ let (led, summary, tooltip) = match (&hello, running) {+ (Some((h, _)), _) => (+ "green",+ "Connected".to_string(),+ format!(+ "Premiere Pro {} connected via UXP plugin {}. Project: {}",+ h["premiereVersion"].as_str().unwrap_or("?"),+ h["pluginVersion"].as_str().unwrap_or("?"),+ h["project"].as_str().unwrap_or("(none open)")+ ),+ ),+ (None, true) if cep => (+ "yellow",+ "Helper only".to_string(),+ "Premiere is running and the Adom CEP helper is connected, but the main UXP plugin isn't. Open Window > UXP Plugins > Adom Bridge once (it then auto-restores).".to_string(),+ ),+ (None, true) => (+ "yellow",+ "Plugin not connected".to_string(),+ "Premiere is running but the Adom UXP plugin hasn't dialed in. Run premiere_install_plugin, then open Window > UXP Plugins > Adom Bridge once.".to_string(),+ ),+ (None, false) => (+ "yellow",+ "Premiere not running".to_string(),+ "Bridge is up. Premiere Pro isn't running — premiere_launch starts it.".to_string(),+ ),+ };+ Json(json!({+ "ok": true,+ "name": "premiere",+ "version": VERSION,+ "led": led,+ "summary": summary,+ "tooltip": tooltip,+ "pluginConnected": hello.is_some(),+ "cepHelperConnected": cep,+ "premiereRunning": running,+ "uptimeSec": st.started.elapsed().as_secs(),+ }))+}++// ───────────────────────────── /command ─────────────────────────────++async fn command(State(st): State<Shared>, method: Method, headers: HeaderMap, body: Bytes) -> Response {+ // Loopback is not an auth boundary: a web page can POST here. Refuse anything that+ // isn't a JSON POST *before* touching the body, and never send ACAO.+ if method != Method::POST {+ return (StatusCode::METHOD_NOT_ALLOWED, [(header::ALLOW, "POST")], "POST only").into_response();+ }+ let is_json = headers+ .get(header::CONTENT_TYPE)+ .and_then(|v| v.to_str().ok())+ .map(|v| v.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case("application/json"))+ .unwrap_or(false);+ if !is_json {+ return (+ StatusCode::UNSUPPORTED_MEDIA_TYPE,+ "Expected request with `Content-Type: application/json`",+ )+ .into_response();+ }+ let req: Value = match serde_json::from_slice(&body) {+ Ok(v) => v,+ Err(e) => {+ return Json(json!({"success": false, "error": format!("bad JSON: {e}"), "errorCode": "bad_request"}))+ .into_response()+ }+ };+ let verb = req["command"].as_str().unwrap_or("").to_string();+ let args = match &req["args"] {+ Value::Null => json!({}),+ Value::String(s) => serde_json::from_str(s).unwrap_or_else(|_| json!({})),+ v => v.clone(),+ };+ Json(dispatch(&st, &verb, args).await).into_response()+}++/// Verb names are accepted with or without the `premiere_` prefix. Boxed because local+/// composite verbs (preview_frame, launch) dispatch other verbs.+pub fn dispatch<'a>(st: &'a Shared, verb: &'a str, args: Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = Value> + Send + 'a>> {+ Box::pin(async move {+ let bare = verb.strip_prefix(PREFIX).unwrap_or(verb);+ let mut out = match host::local_verb(st, bare, &args).await {+ Some(v) => v,+ None => forward(st, bare, args).await,+ };+ catalog::decorate(bare, &mut out);+ out+ })+}++async fn forward(st: &Shared, bare: &str, args: Value) -> Value {+ let Some(spec) = catalog::find(bare) else {+ return json!({+ "success": false,+ "errorCode": "unknown_verb",+ "error": format!("unknown verb premiere_{bare}"),+ "_hint": "Call premiere_describe for the full verb catalog. Anything the catalog doesn't cover can be done with premiere_eval (raw JS against the premierepro UXP API).",+ });+ };+ let timeout = Duration::from_secs(+ args["timeoutSec"].as_u64().unwrap_or(spec.timeout_sec).clamp(1, 3600),+ );+ let args = host::fill_defaults(st, bare, args);+ let runtime = match spec.runtime {+ catalog::Runtime::Uxp | catalog::Runtime::Local => "uxp",+ catalog::Runtime::Cep => "cep",+ // True QE razor beats the UXP emulation when the helper is there.+ catalog::Runtime::PreferCep => {+ if st.runtime_connected("cep").await { "cep" } else { "uxp" }+ }+ };+ match st.call_runtime(runtime, bare, args, timeout).await {+ Ok(res) => {+ if res["ok"].as_bool().unwrap_or(false) {+ let mut out = json!({"success": true});+ merge_result(&mut out, res.get("result").cloned().unwrap_or(Value::Null));+ if let Some(w) = res.get("warnings") {+ out["warnings"] = w.clone();+ }+ out+ } else {+ json!({+ "success": false,+ "error": res["error"].as_str().unwrap_or("plugin error"),+ "errorCode": res["errorCode"].as_str().unwrap_or("premiere_error"),+ "stack": res.get("stack").cloned().unwrap_or(Value::Null),+ })+ }+ }+ Err(CallError::NotConnected) if runtime == "cep" => host::cep_not_connected_response(st).await,+ Err(CallError::NotConnected) => host::not_connected_response(st).await,+ Err(CallError::Disconnected) => json!({+ "success": false,+ "errorCode": "plugin_disconnected",+ "error": "Premiere's UXP plugin disconnected mid-call (Premiere quit, crashed, or the plugin reloaded).",+ "_hint": "Check premiere_status. If Premiere is still up, the plugin reconnects within ~3s — retry once. If the verb mutated the timeline, call premiere_get_sequence before retrying so you don't apply it twice.",+ }),+ Err(CallError::Timeout(s)) => json!({+ "success": false,+ "errorCode": "plugin_timeout",+ "stillRunning": true,+ "statusVerb": "premiere_status",+ "error": format!("Premiere didn't answer within {s}s. A modal dialog in Premiere blocks scripting, or the operation is genuinely long."),+ "_hint": "Call premiere_status (shows whether Premiere is responsive). For long exports use premiere_export_sequence (it returns immediately and you poll premiere_export_status). You can pass a bigger timeoutSec.",+ }),+ }+}++/// Plugin results that are objects get flattened into the response; anything else+/// lands under `result`.+fn merge_result(out: &mut Value, result: Value) {+ match result {+ Value::Object(map) => {+ for (k, v) in map {+ if k != "success" {+ out[k] = v;+ }+ }+ }+ Value::Null => {}+ other => out["result"] = other,+ }+}++// ───────────────────────────── /ws (plugin) ─────────────────────────────++async fn ws_upgrade(+ State(st): State<Shared>,+ Query(q): Query<HashMap<String, String>>,+ ws: WebSocketUpgrade,+) -> Response {+ // WebSockets are NOT covered by CORS, so any web page could open one. The token+ // (only readable from the user's ~/.adom dir) is what keeps strangers out.+ if q.get("token").map(String::as_str) != Some(st.token.as_str()) {+ return (StatusCode::UNAUTHORIZED, "bad token").into_response();+ }+ ws.max_message_size(256 * 1024 * 1024)+ .on_upgrade(move |sock| plugin_session(st, sock))+}++static CONN_SEQ: AtomicU64 = AtomicU64::new(1);++async fn plugin_session(st: Shared, sock: WebSocket) {+ let (mut sink, mut stream) = sock.split();+ let (tx, mut rx) = mpsc::unbounded_channel::<String>();+ let conn_id = CONN_SEQ.fetch_add(1, Ordering::Relaxed);++ let writer = tokio::spawn(async move {+ while let Some(msg) = rx.recv().await {+ if sink.send(Message::Text(msg)).await.is_err() {+ break;+ }+ }+ });++ // Keepalive: UXP sockets die silently when Premiere sleeps; a ping every 15s+ // surfaces that as a disconnect instead of a hung call.+ let ka_tx = tx.clone();+ let keepalive = tokio::spawn(async move {+ loop {+ tokio::time::sleep(Duration::from_secs(15)).await;+ if ka_tx.send(json!({"type": "ping"}).to_string()).is_err() {+ break;+ }+ }+ });++ while let Some(Ok(msg)) = stream.next().await {+ let text = match msg {+ Message::Text(t) => t,+ Message::Binary(b) => String::from_utf8_lossy(&b).into_owned(),+ Message::Close(_) => break,+ _ => continue,+ };+ let Ok(v) = serde_json::from_str::<Value>(&text) else { continue };+ match v["type"].as_str() {+ Some("hello") => {+ let runtime = if v["runtime"].as_str() == Some("cep") { "cep" } else { "uxp" }.to_string();+ eprintln!("[premiere-bridge] {runtime} connected: {v}");+ // A newer connection replaces an older one of the same runtime (plugin+ // reloaded, a second panel instance).+ st.plugins.lock().await.insert(+ runtime.clone(),+ PluginConn { runtime, tx: tx.clone(), hello: v.clone(), connected_at: Instant::now(), conn_id },+ );+ let _ = tx.send(json!({"type": "welcome", "bridgeVersion": VERSION, "protocol": PROTOCOL}).to_string());+ }+ Some("result") => {+ if let Some(id) = v["id"].as_u64() {+ if let Some((_, waiter)) = st.pending.lock().await.remove(&id) {+ let _ = waiter.send(v);+ }+ }+ }+ Some("event") => {+ let seq = st.event_seq.fetch_add(1, Ordering::Relaxed) + 1;+ let mut ev = v.clone();+ ev["seq"] = json!(seq);+ let mut events = st.events.lock().await;+ events.push(ev);+ let len = events.len();+ if len > 200 {+ events.drain(..len - 200);+ }+ }+ Some("hello_update") => {+ if let Some(p) = st.plugins.lock().await.values_mut().find(|p| p.conn_id == conn_id) {+ {+ if let (Some(dst), Some(src)) = (p.hello.as_object_mut(), v["fields"].as_object()) {+ for (k, val) in src {+ dst.insert(k.clone(), val.clone());+ }+ }+ }+ }+ }+ _ => {}+ }+ }++ keepalive.abort();+ writer.abort();+ let mut guard = st.plugins.lock().await;+ let gone: Vec<String> = guard.values().filter(|p| p.conn_id == conn_id).map(|p| p.runtime.clone()).collect();+ for rt in &gone {+ guard.remove(rt);+ eprintln!("[premiere-bridge] {rt} disconnected");+ }+ drop(guard);+ // Dropping this connection's waiters wakes its in-flight calls with Disconnected+ // instead of letting them sit until their timeout.+ if !gone.is_empty() {+ st.pending.lock().await.retain(|_, (owner, _)| *owner != conn_id);+ }+}
src/plugin_pkg.rsadded+205@@ -0,0 +1,205 @@+//! Installing the two in-app halves. Both are compiled INTO this binary (include_str!),+//! so the Release zip stays a single executable + bridge.json and a bridge update always+//! carries the matching plugin.+//!+//! * UXP plugin → packaged as a .ccx and installed with Adobe's own+//! UnifiedPluginInstallerAgent (UPIA) — no dialogs, no Developer Tool.+//! * CEP helper → copied into the per-user CEP extensions folder; unsigned CEP needs+//! PlayerDebugMode=1 for the CSXS versions Premiere uses.++use crate::Shared;+use serde_json::{json, Value};+use std::{+ io::Write,+ path::{Path, PathBuf},+};+use tokio::process::Command;++pub const BUNDLED_VERSION: &str = "0.1.0";+const UXP_ID: &str = "inc.adom.premiere.bridge";+const UXP_NAME: &str = "Adom Bridge";+const CEP_ID: &str = "inc.adom.premiere.cep";++const UXP_FILES: &[(&str, &str)] = &[+ ("manifest.json", include_str!("../plugin/uxp/manifest.json")),+ ("index.html", include_str!("../plugin/uxp/index.html")),+ ("index.js", include_str!("../plugin/uxp/index.js")),+];++const UXP_ICONS: &[(&str, &[u8])] = &[+ ("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),+ ("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),+ ("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),+ ("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),+];++const CEP_FILES: &[(&str, &str)] = &[+ ("CSXS/manifest.xml", include_str!("../plugin/cep/CSXS/manifest.xml")),+ ("index.html", include_str!("../plugin/cep/index.html")),+ ("main.js", include_str!("../plugin/cep/main.js")),+ ("host.jsx", include_str!("../plugin/cep/host.jsx")),+];++fn record_path(st: &Shared) -> PathBuf {+ st.data_dir.join("plugin-installed.json")+}++pub fn installed_version() -> Option<String> {+ let p = crate::host::data_dir().join("plugin-installed.json");+ let v: Value = serde_json::from_slice(&std::fs::read(p).ok()?).ok()?;+ v["version"].as_str().map(str::to_string)+}++fn upia_path() -> Option<PathBuf> {+ let p = if cfg!(target_os = "macos") {+ PathBuf::from("/Library/Application Support/Adobe/Adobe Desktop Common/RemoteComponents/UPI/UnifiedPluginInstallerAgent/UnifiedPluginInstallerAgent.app/Contents/MacOS/UnifiedPluginInstallerAgent")+ } else {+ let cpf = std::env::var("CommonProgramFiles").unwrap_or_else(|_| r"C:\Program Files\Common Files".into());+ PathBuf::from(cpf).join(r"Adobe\Adobe Desktop Common\RemoteComponents\UPI\UnifiedPluginInstallerAgent\UnifiedPluginInstallerAgent.exe")+ };+ p.exists().then_some(p)+}++fn cep_dir() -> PathBuf {+ if cfg!(windows) {+ let appdata = std::env::var("APPDATA").map(PathBuf::from).unwrap_or_else(|_| crate::host::home_dir().join("AppData/Roaming"));+ appdata.join(r"Adobe\CEP\extensions").join(CEP_ID)+ } else {+ crate::host::home_dir().join("Library/Application Support/Adobe/CEP/extensions").join(CEP_ID)+ }+}++/// Build the .ccx (a zip with manifest.json at the root). A copy of the connection file+/// rides inside so the plugin can find us even if it can't read ~/.adom.+fn build_ccx(st: &Shared) -> std::io::Result<PathBuf> {+ let out = st.data_dir.join(format!("{UXP_ID}.ccx"));+ let f = std::fs::File::create(&out)?;+ let mut z = zip::ZipWriter::new(f);+ let opts: zip::write::SimpleFileOptions = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);+ for (name, body) in UXP_FILES {+ z.start_file(*name, opts).map_err(std::io::Error::other)?;+ z.write_all(body.as_bytes())?;+ }+ for (name, body) in UXP_ICONS {+ z.start_file(*name, opts).map_err(std::io::Error::other)?;+ z.write_all(body)?;+ }+ let conn = std::fs::read(st.data_dir.join("connection.json")).unwrap_or_default();+ z.start_file("connection.json", opts).map_err(std::io::Error::other)?;+ z.write_all(&conn)?;+ z.finish().map_err(std::io::Error::other)?;+ Ok(out)+}++async fn run(cmd: &Path, args: &[&str]) -> (bool, String) {+ match Command::new(cmd).args(args).output().await {+ Ok(o) => {+ let mut s = String::from_utf8_lossy(&o.stdout).into_owned();+ s.push_str(&String::from_utf8_lossy(&o.stderr));+ (o.status.success(), s.trim().to_string())+ }+ Err(e) => (false, e.to_string()),+ }+}++async fn install_uxp(st: &Shared, force: bool) -> Value {+ let Some(upia) = upia_path() else {+ return json!({"ok": false, "errorCode": "upia_missing",+ "error": "Adobe's UnifiedPluginInstallerAgent wasn't found (it ships with the Creative Cloud desktop app)."});+ };+ let ccx = match build_ccx(st) {+ Ok(p) => p,+ Err(e) => return json!({"ok": false, "errorCode": "ccx_build_failed", "error": e.to_string()}),+ };+ let flag = |mac: &'static str, win: &'static str| if cfg!(windows) { win } else { mac };+ let mut log = Vec::new();+ if force {+ let (_, out) = run(&upia, &[flag("--remove", "/remove"), UXP_NAME]).await;+ log.push(json!({"step": "remove", "output": out}));+ }+ let ccx_s = ccx.to_string_lossy().into_owned();+ let (ok, out) = run(&upia, &[flag("--install", "/install"), &ccx_s]).await;+ log.push(json!({"step": "install", "ok": ok, "output": out}));+ // UPIA's exit code is unreliable across versions; its text is the real signal.+ let lower = out.to_lowercase();+ let success = ok && !lower.contains("fail") && !lower.contains("error") || lower.contains("successfully installed") || lower.contains("installation successful");+ json!({"ok": success, "ccx": ccx, "upia": upia, "log": log})+}++async fn install_cep() -> Value {+ let dir = cep_dir();+ for (name, body) in CEP_FILES {+ let p = dir.join(name);+ if let Some(parent) = p.parent() {+ if let Err(e) = std::fs::create_dir_all(parent) {+ return json!({"ok": false, "errorCode": "cep_copy_failed", "error": e.to_string()});+ }+ }+ if let Err(e) = std::fs::write(&p, body) {+ return json!({"ok": false, "errorCode": "cep_copy_failed", "error": e.to_string()});+ }+ }+ // Unsigned CEP extensions only load with PlayerDebugMode=1. Set it for every CSXS+ // generation Premiere 2022–2027 might use; it's per-user and harmless for others.+ let mut debug = Vec::new();+ for n in 9..=13 {+ let ok = if cfg!(windows) {+ let key = format!(r"HKCU\Software\Adobe\CSXS.{n}");+ Command::new("reg").args(["add", &key, "/v", "PlayerDebugMode", "/t", "REG_SZ", "/d", "1", "/f"]).output().await.map(|o| o.status.success()).unwrap_or(false)+ } else {+ Command::new("defaults").args(["write", &format!("com.adobe.CSXS.{n}"), "PlayerDebugMode", "1"]).output().await.map(|o| o.status.success()).unwrap_or(false)+ };+ debug.push(json!({"csxs": n, "ok": ok}));+ }+ json!({"ok": true, "dir": dir, "playerDebugMode": debug})+}++pub async fn install(st: &Shared, args: &Value) -> Value {+ let force = args["force"].as_bool().unwrap_or(false);+ let uxp = install_uxp(st, force).await;+ let cep = if args["cep"].as_bool().unwrap_or(true) { install_cep().await } else { json!({"ok": false, "skipped": true}) };+ let uxp_ok = uxp["ok"].as_bool().unwrap_or(false);+ if uxp_ok {+ let _ = std::fs::write(+ record_path(st),+ serde_json::to_vec_pretty(&json!({"version": BUNDLED_VERSION, "uxp": uxp["ccx"], "cep": cep["dir"]})).unwrap_or_default(),+ );+ }+ let running = crate::host::premiere_running().await;+ let mut out = json!({+ "success": uxp_ok,+ "version": BUNDLED_VERSION,+ "uxp": uxp,+ "cep": cep,+ "premiereRunning": running,+ });+ if uxp_ok {+ out["_hint"] = json!(if running {+ "Installed. Premiere is running, so it hasn't loaded the plugin yet: premiere_quit {save:true} then premiere_launch. After launch, if pluginConnected stays false, the Adom Bridge panel needs opening once (Window > UXP Plugins > Adom Bridge) — the CEP helper connects on its own meanwhile."+ } else {+ "Installed. premiere_launch next; the plugin connects on startup."+ });+ out["_next"] = json!(if running { json!(["premiere_quit", "premiere_launch"]) } else { json!(["premiere_launch"]) });+ } else {+ out["errorCode"] = uxp["errorCode"].clone();+ out["error"] = json!("UXP plugin install failed — see uxp.log for the installer's output.");+ out["_hint"] = json!(format!(+ "If UPIA refused the unsigned .ccx, the package is at {} — opening it with the Creative Cloud app installs it (one confirmation click). Retry with force:true if an older copy is in the way.",+ uxp["ccx"].as_str().unwrap_or("?")+ ));+ }+ out+}++pub async fn info(st: &Shared) -> Value {+ json!({+ "success": true,+ "bundledVersion": BUNDLED_VERSION,+ "installedVersion": installed_version(),+ "uxpPluginId": UXP_ID,+ "upia": upia_path(),+ "cepDir": cep_dir(),+ "cepInstalled": cep_dir().join("CSXS/manifest.xml").exists(),+ "ccx": st.data_dir.join(format!("{UXP_ID}.ccx")),+ })+}
uninstall.shadded+8@@ -0,0 +1,8 @@+#!/usr/bin/env bash+set -euo pipefail+remove_skill() { for home in "$HOME/.claude/skills" "$HOME/.codex/skills"; do rm -rf "${home:?}/$1"; done; }+remove_skill premiere-bridge-pack+remove_skill premiere-bridge+remove_skill premiere-editing+remove_skill premiere-preview-export+echo "premiere-bridge skills removed"