feat: initial commit - Jhonny Editor

- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
This commit is contained in:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
# UXP CCX distribution
The UXP bridge is packaged as a `.ccx` candidate for direct installation
through Adobe Creative Cloud Desktop. It targets Premiere Pro 25.6+ only. The
CI packer creates a deterministic ZIP-based CCX structure without requiring
UXP Developer Tool, but Adobe's own tooling and a real Premiere installation
remain the authoritative installation check.
## Build channels
Validate the source manifest and build the normal direct-distribution package:
```sh
node scripts/validate-distribution.mjs --uxp
node scripts/build-uxp-ccx.mjs
```
This writes:
```text
artifacts/premiere-pro-mcp-uxp-<version>-direct.ccx
```
The builder uses a fixed ZIP timestamp, lexical file order, and stored entries,
then reads the generated central directory and checks every entry and CRC. The
result is reproducible for identical source bytes. It also rejects symlinks,
requires the UXP v5 Premiere manifest, checks the version against
`package.json`. Adobe's Premiere 26.3 runtime requires the compatible `domains: "all"`
manifest form for the WebSocket connection; `workspace.cjs` remains the enforced authority and
accepts only `ws://127.0.0.1:<port>/uxp` or `ws://localhost:<port>/uxp`.
For an Adobe Creative Cloud Marketplace submission, Adobe Developer
Distribution must first create the Marketplace plugin ID. Build a separate
package without changing the checked-in direct manifest:
```sh
UXP_DISTRIBUTION_CHANNEL=marketplace \
UXP_MARKETPLACE_PLUGIN_ID='<Adobe-issued ID>' \
node scripts/build-uxp-ccx.mjs
```
In PowerShell:
```powershell
$env:UXP_DISTRIBUTION_CHANNEL = 'marketplace'
$env:UXP_MARKETPLACE_PLUGIN_ID = '<Adobe-issued ID>'
node scripts/build-uxp-ccx.mjs
```
That produces `artifacts/premiere-pro-mcp-uxp-<version>-marketplace.ccx`. The
script refuses a Marketplace build without an Adobe-issued ID or if that ID is
the direct-distribution ID. This preserves Adobe's required split between
direct and Marketplace identities when both channels are used.
## CI and approval boundaries
`.github/workflows/uxp-package.yml` builds and uploads the direct CCX on every
published release. It can also be run manually for a Marketplace candidate
after the owner supplies the Adobe-issued ID. The workflow only produces
artifacts; it does not create a Developer Distribution listing, submit an
artifact, make a Marketplace listing public, or claim Adobe approval.
Adobe recommends packaging through UXP Developer Tool for normal release work.
This repository's deterministic packer is intended for CI artifact creation
because a CCX is a ZIP archive, but it does not replace these required release
gates:
1. Install the resulting CCX through Creative Cloud Desktop on Windows, Intel
macOS, and Apple Silicon as applicable.
2. Load the panel in a real Premiere Pro 25.6+ host and run the documented
read-only connection check.
3. For Marketplace distribution, use the exact ID from Adobe Developer
Distribution, complete listing metadata and reviewer instructions, and wait
for Adobe review and approval.
Adobe states that ordinary UXP CCX packages do not use the CEP-style digital
signature/timestamp process. Direct installs may still show Creative Cloud's
trust warning, so distribute the artifact only from a source users can verify.
See Adobe's [packaging guide](https://developer.adobe.com/premiere-pro/uxp/plugins/distribution/package/)
and [Marketplace guide](https://developer.adobe.com/premiere-pro/uxp/plugins/distribution/adobe-marketplace/).
## Capability boundary
The CCX package being well-formed does not establish live-host capability
parity. UXP is the supported path for the operations its runtime capability
probe advertises. CEP remains the explicit compatibility path for older hosts
and unsupported UXP operations; a failed UXP mutation must never be silently
retried through CEP.
+149
View File
@@ -0,0 +1,149 @@
# MCP for Adobe Premiere Pro UXP bridge
Production-oriented UXP transport for Premiere Pro 25.6+. Set `PREMIERE_UXP_TOKEN` to a secret of at least 16 characters before starting the MCP server. Side-load `manifest.json` with UXP Developer Tool during development, open **Window → UXP Plugins → MCP for Adobe Premiere Pro**, enter the same secret in **Bridge token**, then connect to the MCP-side WebSocket endpoint (default `ws://127.0.0.1:7777/uxp`). For end-user direct distribution, use the `.ccx` workflow in [DISTRIBUTION.md](./DISTRIBUTION.md) instead of asking users to load a development folder.
Before using a path-based command, choose one **Approved workspace** folder in the
panel. The manifest uses requested filesystem access, and the panel restores only
Adobe's opaque persistent folder token. Proxy, relink, preset, interchange, AAF,
frame-export, and Source Monitor file paths must remain inside that folder. The
token and native root path are never sent to the MCP server.
The bridge sends a versioned `hello`, subscribes to Premiere's documented global project and sequence events, emits `premiere.state.changed` notifications, and accepts only commands that its runtime capability probe declares supported. A five-second deduplicated poll remains as a fallback for state such as playhead movement that has no matching documented event.
It also exposes documented Premiere 25.6+ video-transition and transcript workflows:
- `timeline.selection.lift` removes the current timeline selection without ripple through one undoable transaction; the committed transaction is not a timeline readback
- `transition.video.list`, `transition.video.add`, and `transition.video.remove`
- `transcript.export`, `transcript.search`, `transcript.has`, and guarded undoable `transcript.import`
- read-only `captions.inspect`
Transition names must come from `transition.video.list`. Transcript inspection targets can be selected in the Project panel or addressed by project-item ID/name. Transcript import is stricter: it requires an exact project GUID and project-item ID plus a current transcript SHA-256 (or an explicit absent-transcript precondition), confirmation, and an operation ID; it never uses selection or a name to mutate. Caption creation, text/timing mutation, and deletion remain explicitly unsupported because Premiere does not document those UXP APIs.
The MCP adapter exposes `transcript.export` as `get_clip_transcript_uxp` and adds a
SHA-256 revision to the result. `search_clip_transcript_uxp` maps to the existing
read-only search command. `preview_transcript_edit_uxp` re-exports the transcript,
rejects stale revisions, validates and merges selected source-time deletion ranges,
and returns a confirmation token. It never changes the transcript or timeline;
automatic transcript-to-timeline application remains a live-host validation gate.
`import_transcript_uxp` is the guarded MCP facade for `transcript.import`: it
caps the replacement JSON at 24 KiB UTF-8, serializes import requests per source
clip, commits one native transaction, and only calls the result verified after a
bounded native export has the exact requested SHA-256. A failed post-commit
readback remains `committed_unverified`; static tests do not prove host acceptance,
persistence, Undo, or licensed-host behavior.
## Premiere 26.3 commands
The following commands use APIs Adobe introduced in Premiere 26.3. They are
available only when the connected host advertises them through `capabilities.get`:
- `track.rename` — rename one `video`, `audio`, or `caption` track using an
undoable `createSetNameAction` transaction. The post-transaction read-back is
the verification evidence.
- `subclip.create` — create a subclip from a clip project item with non-negative,
ordered in/out points, hard-boundary selection, and optional audio/video
inclusion. It is an undoable action and must return the created item identity.
- `marker.list` — return sequence or resolved project-item markers, including the
stable marker `guid` introduced in 26.3. It is read-only; documented web links
and raw RGBA color components are opt-in, and raw color values make no
color-profile or rendered-appearance claim.
- `sourceMonitor.position.set` — set the Source Monitor using a `TickTime` and
read the position back. It changes monitor state, not project edit history.
- `transcript.has` — report whether a resolved `ClipProjectItem` has a
transcript. It is read-only and does not start Speech-to-Text.
- `transcript.import` — replace one exact clip transcript through a revision-locked,
explicitly confirmed 26.3+ transaction with bounded export readback.
- `interchange.aaf.export` — export the active sequence through
`ProjectConverter.exportAAF` with a bounded, documented `AAFExportOptions`
schema. Premiere's boolean result is recorded, but output-file inspection must
still be completed by the host gate.
These map to MCP tools `rename_track_uxp`, `create_subclip_uxp`,
`list_markers_uxp`, `set_source_monitor_position_uxp`, `has_transcript_uxp`, and
`import_transcript_uxp`, and `export_aaf_uxp`. See [the 26.3 coverage matrix](../docs/adobe-uxp-26.3-coverage.md)
for command arguments, support states, and primary Adobe references.
## Stable workflow commands
The panel also exposes runtime-probed commands for native audio/video component
chains, deterministic timeline selection, compound effect batches over the current
timeline selection, scene-edit
detection, proxy and ingest state, guarded offline relink, transactional project/XMP
metadata, footage interpretation and LUTs, Source Monitor audition, and
project/Production scratch-disk preflight. They map to eleven consolidated MCP tools;
see [the stable workflow matrix](../docs/uxp-stable-workflows.md) for exact commands,
arguments, confirmation requirements, and the pending live-host gate.
A second stable expansion adds Project-view selection, native marker CRUD, bin
organization, sequence settings profiles, workspace-gated imports, typed parameter,
keyframe, and animation-mode automation, track-item transformations, documented SequenceEditor
edits, sequence lifecycle management, and AME encoding. These map to another ten
consolidated MCP tools and 45 capability-probed panel commands. See [the next-ten
workflow matrix](../docs/uxp-next-ten-workflows.md). Automated contract evidence is
complete; live Premiere verification remains pending.
The third-wave command surface adds bounded event and AME receipts, readiness waits,
safe multi-project sessions, a growing-media pause lease, workflow checkpoints,
media-health maintenance, caption-aware track state, and transactional source
trim/framing. See [the third-wave workflow matrix](../docs/third-wave-uxp-workflows.md)
for the verification boundary of every operation.
`hybrid-benchmark.cjs` is a dormant developer benchmark. The production manifest
does not request `enableAddon`, declare an addon, or ship a native binary. A future
native-acceleration change must first pass the documented Windows x64, macOS x64,
and macOS arm64 Release evidence gate; see
[the hybrid benchmark procedure](../docs/uxp-hybrid-benchmark.md).
`frame.export` uses Adobe's supported `Exporter.exportSequenceFrame()`. The public request and returned path include one `.png` extension; the panel passes the bare stem to Premiere because its exporter appends the extension. Premiere must confirm the export request before a path is reported. A licensed-host file check remains required to prove the artifact exists on disk. Example:
```json
{"type":"command","requestId":"42","command":"frame.export","args":{"outputDirectory":"C:/temp","filename":"frame.png"}}
```
## Operation lifecycle and guarantees
Commands emit correlated lifecycle events:
- `premiere.operation.started`
- `premiere.operation.progress`
- `premiere.operation.completed`
- `premiere.operation.failed`
- `premiere.operation.cancelled`
Each result includes structured operation metadata covering project mutation, verification evidence, undo, transaction, and cancellation boundaries. These fields describe evidence, not intent:
- Frame export is not a project mutation, is not undoable, and verifies success by checking the output file.
- `operation.cancel` is cooperative during preflight. Once a Premiere host call begins, the bridge reports `host_call_not_cancellable` and does not claim interruption.
- Action-based commands use `project.lockedAccess()` to create the action and
`project.executeTransaction()` to consume it without asynchronous work or an
escaped action object. Only those commands may claim a Premiere undo-history or
transaction boundary.
- The bridge does not claim atomic rollback. Failed operations must be inspected using their verification metadata before retrying.
Example cancellation request:
```json
{"type":"command","requestId":"cancel-42","command":"operation.cancel","args":{"requestId":"42"}}
```
## CEP fallback contract
Capability discovery is authoritative per host session. Route a command to CEP only when the UXP capability is absent or explicitly `supported: false`. Never replay a failed UXP mutation through CEP automatically: the first attempt may have partially succeeded. CEP/QE results must identify `backend: "cep"`, and callers should treat QE-only behavior as compatibility mode rather than equivalent proof. UXP remains authoritative for supported frame export.
Loopback WebSocket support must still be verified on each supported OS/Premiere combination. Until that evidence exists, keep the existing file bridge available as the transport fallback; transport fallback does not change command-backend selection.
The event and transaction boundaries follow Adobe's documented [EventManager](https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/eventmanager), [Project.executeTransaction](https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/project), and [CompoundAction](https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/compoundaction) APIs.
Adobe's published [`@adobe/premierepro`](https://www.npmjs.com/package/@adobe/premierepro)
26.3.0 package is the stable declaration baseline for this bridge. The `beta` tag
(currently a 26.5 prerelease) is deliberately excluded from capability claims and
release support until Adobe ships a stable host API and this panel passes the same
live-host gate. The official [Premiere UXP ESLint rules](https://developer.adobe.com/premiere-pro/uxp/resources/fundamentals/eslint-support/)
are the intended static guard against lock, transaction, async-callback, and
action-lifetime violations; static lint and unit tests are not Premiere-host proof.
## MCP-side transport
The server listener binds only to `127.0.0.1`, requires the token during the WebSocket upgrade, limits messages to 1 MiB, and requires a versioned UXP `hello` before accepting results. Requests are correlated by random IDs and fail on timeout or disconnect.
When `PREMIERE_UXP_TOKEN` is configured, the MCP server registers only commands implemented by this panel. These tools never silently replay a failed UXP request through CEP. `PREMIERE_UXP_PORT` changes the loopback port when `7777` is unavailable. The server adapter accepts protocol versions 1 and 2; this integrated panel emits protocol version 2.
File diff suppressed because it is too large Load Diff
+344
View File
@@ -0,0 +1,344 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpCloneWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// This is intentionally an append-only clone. SequenceEditor can clone at
// arbitrary offsets and tracks, but a coordinate-bound append lets the
// bridge prove both the original and exactly one new item without scanning
// or overwriting unrelated timeline content.
function createCloneWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const fallbackTails = new Map();
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
? deps.locks
: { withTrackMutationLock: localLock };
const definitions = {
"trackItem.clone.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTrackItemClone,
handler: inspectTrackItemClone
},
"trackItem.clone": {
destructive: true,
undoable: true,
idempotent: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTrackItemClone,
handler: cloneTrackItem
}
};
function canUseTrackItemClone() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.SequenceEditor && typeof ppro.SequenceEditor.getEditor === "function" &&
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
}
async function inspectTrackItemClone(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
const context = await activeTarget(args, false);
const snapshot = await cloneSnapshot(context);
assertAppendSupported(snapshot);
return snapshot;
}
async function cloneTrackItem(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "confirmDuplicate", "operationId"]);
requireConfirmation(args.confirmDuplicate);
requireOperationId(args.operationId);
const target = targetCoordinates(args);
const expected = requiredSnapshot(args.expectedSnapshot);
assertExpectedTarget(expected, target);
const initial = await activeTarget(target, true);
if (initial.projectGuid !== expected.projectGuid || initial.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed duplicate snapshot");
}
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
return locks.withTrackMutationLock(key, async function () {
// Read only after entering the same track tail used by the guarded
// slip and slide workflows. A distinct operation ID cannot pass an
// earlier inspected coordinate into action creation.
const context = await activeTarget(target, true);
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the duplicate transaction");
}
const before = await cloneSnapshot(context);
if (!sameSnapshot(before, expected)) {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed source item or append boundary changed since inspection");
}
assertAppendSupported(before);
let committed = false;
context.project.lockedAccess(function () {
// The complete asynchronous snapshot is immediately followed by
// this synchronous check/action construction boundary. This avoids
// creating an Action from a stale active project or sequence.
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before duplicate action creation");
}
const editor = ppro.SequenceEditor.getEditor(context.sequence);
if (!editor || typeof editor.createCloneTrackItemAction !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose SequenceEditor clone actions");
}
const action = editor.createCloneTrackItemAction(
context.item,
ppro.TickTime.createWithSeconds(before.source.durationSeconds),
0,
0,
false,
true
);
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the duplicate action");
committed = context.project.executeTransaction(function (compoundAction) {
if (compoundAction.addAction(action) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the duplicate action");
}
}, "Duplicate timeline item after source");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the duplicate transaction");
// Re-resolve by the original coordinate plus the one deterministic
// append coordinate. Do not walk or read unrelated item fields after
// a committed action.
const afterContext = await activeTarget(target, true);
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed duplicate");
}
const after = await cloneReadback(afterContext, before);
if (!sameCloneResult(before, after)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested append-only duplicate");
}
return {
duplicated: true,
before,
after,
outcome: "verified",
verificationBoundary: "source_and_appended_track_item_readback",
undoLabel: "Duplicate timeline item after source"
};
});
}
async function activeTarget(args, requireTransaction) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
const target = targetCoordinates(args), resolved = await trackItemsAt(sequence, target);
const item = resolved.items[target.clipIndex];
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
return {
project,
sequence,
projectGuid: requiredGuid(project.guid, "active project GUID"),
sequenceId: requiredGuid(sequence.guid, "active sequence GUID"),
...target,
item,
items: resolved.items
};
}
async function trackItemsAt(sequence, target) {
const title = target.mediaType === "video" ? "Video" : "Audio";
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
}
const count = Number(await sequence[countMethod]());
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (!items.length) throw commandError("UXP_TARGET_NOT_FOUND", "The target track has no clip items");
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
return { items };
}
async function cloneSnapshot(context) {
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
clipIndex: context.clipIndex,
trackItemCount: context.items.length,
source: await itemSnapshot(context.item, "source")
};
}
async function cloneReadback(context, before) {
const items = context.items;
const source = items[before.clipIndex], duplicate = items[before.clipIndex + 1];
if (!source || !duplicate) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not expose both source and appended duplicate coordinates");
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
sourceClipIndex: before.clipIndex,
duplicateClipIndex: before.clipIndex + 1,
trackItemCount: items.length,
source: await itemSnapshot(source, "source readback"),
duplicate: await itemSnapshot(duplicate, "duplicate readback")
};
}
async function itemSnapshot(item, label) {
const sourceItem = await requiredMethod(item, "getProjectItem")();
const snapshot = {
projectItemId: requiredIdentifier(await requiredMethod(sourceItem, "getId")(), label + " source project-item ID"),
startSeconds: tickSeconds(await requiredMethod(item, "getStartTime")()),
endSeconds: tickSeconds(await requiredMethod(item, "getEndTime")()),
inSeconds: tickSeconds(await requiredMethod(item, "getInPoint")()),
outSeconds: tickSeconds(await requiredMethod(item, "getOutPoint")()),
durationSeconds: tickSeconds(await requiredMethod(item, "getDuration")()),
speed: Number(await requiredMethod(item, "getSpeed")()),
reversed: Boolean(await requiredMethod(item, "isSpeedReversed")())
};
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " " + key);
}
}
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " speed");
}
return snapshot;
}
function assertAppendSupported(snapshot) {
if (snapshot.clipIndex !== snapshot.trackItemCount - 1) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Duplicate only appends a copy after the final clip item on the requested track");
}
const source = snapshot.source;
if (source.durationSeconds <= 0 || !numbersEqual(source.endSeconds - source.startSeconds, source.durationSeconds) ||
source.endSeconds + source.durationSeconds > 86400) {
throw commandError("UXP_TARGET_UNSUPPORTED", "The source item cannot be safely appended within the documented timeline bounds");
}
}
function sameCloneResult(before, after) {
const source = before.source, duplicate = after.duplicate;
return after.projectGuid === before.projectGuid && after.sequenceId === before.sequenceId &&
after.mediaType === before.mediaType && after.trackIndex === before.trackIndex &&
after.sourceClipIndex === before.clipIndex && after.duplicateClipIndex === before.clipIndex + 1 &&
after.trackItemCount === before.trackItemCount + 1 && sameItem(after.source, source) &&
duplicate.projectItemId === source.projectItemId &&
numbersEqual(duplicate.startSeconds, source.endSeconds) &&
numbersEqual(duplicate.endSeconds, source.endSeconds + source.durationSeconds) &&
numbersEqual(duplicate.durationSeconds, source.durationSeconds) &&
numbersEqual(duplicate.inSeconds, source.inSeconds) && numbersEqual(duplicate.outSeconds, source.outSeconds) &&
numbersEqual(duplicate.speed, source.speed) && duplicate.reversed === source.reversed;
}
function sameSnapshot(left, right) {
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
left.mediaType === right.mediaType && left.trackIndex === right.trackIndex &&
left.clipIndex === right.clipIndex && left.trackItemCount === right.trackItemCount &&
sameItem(left.source, right.source);
}
function sameItem(left, right) {
return left.projectItemId === right.projectItemId && numbersEqual(left.startSeconds, right.startSeconds) &&
numbersEqual(left.endSeconds, right.endSeconds) && numbersEqual(left.inSeconds, right.inSeconds) &&
numbersEqual(left.outSeconds, right.outSeconds) && numbersEqual(left.durationSeconds, right.durationSeconds) &&
numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
}
function targetCoordinates(args) {
return {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
};
}
function requiredSnapshot(value) {
assertObject(value, "expectedSnapshot");
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "trackItemCount", "source"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
return {
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
trackItemCount: positiveInt(value.trackItemCount, "expectedSnapshot.trackItemCount"),
source: requiredItemSnapshot(value.source, "expectedSnapshot.source")
};
}
function requiredItemSnapshot(value, name) {
assertObject(value, name);
const allowed = ["projectItemId", "startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
return {
projectItemId: requiredIdentifier(value.projectItemId, name + ".projectItemId"),
startSeconds: boundedSeconds(value.startSeconds, name + ".startSeconds"),
endSeconds: boundedSeconds(value.endSeconds, name + ".endSeconds"),
inSeconds: boundedSeconds(value.inSeconds, name + ".inSeconds"),
outSeconds: boundedSeconds(value.outSeconds, name + ".outSeconds"),
durationSeconds: boundedSeconds(value.durationSeconds, name + ".durationSeconds"),
speed: boundedSpeed(value.speed, name + ".speed"),
reversed: requiredBoolean(value.reversed, name + ".reversed")
};
}
function assertExpectedTarget(expected, target) {
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
}
}
function localLock(key, operation) {
const previous = fallbackTails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
fallbackTails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
});
}
function requiredMethod(target, name) { if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The required item does not expose " + name); return target[name].bind(target); }
function assertObject(value, name) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object"); }
function assertOnlyKeys(value, allowed) { assertObject(value, "arguments"); for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }
function enumValue(value, name, allowed) { if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
function nonNegativeInt(value, name) { if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511"); return value; }
function positiveInt(value, name) { if (!Number.isSafeInteger(value) || value < 1 || value > 512) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 1 to 512"); return value; }
function boundedSeconds(value, name) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400"); return seconds; }
function boundedSpeed(value, name) { const speed = Number(value); if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100"); return speed; }
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
function requireConfirmation(value) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "trackItem.clone requires confirmDuplicate: true"); }
function requireOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); }
function requiredGuid(value, name) { const guid = guidString(value); if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return guid; }
function requiredIdentifier(value, name) { const id = guidString(value); if (!id || id.length > 512 || id.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return id; }
function guidString(value) { if (value == null) return ""; try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; } }
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
function numbersEqual(left, right) { return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001; }
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return definitions;
}
return { createCloneWorkflowDefinitions };
});
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpDialogueWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
function createDialogueWorkflowDefinitions(deps) {
const ppro = deps.ppro;
return { "dialogue.deriveSequence": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, minHostVersion: "26.3.0", probe: canDerive, handler: derive } };
function canDerive() { return !!(ppro.Project && ppro.Project.getActiveProject && ppro.SequenceEditor && ppro.SequenceEditor.getEditor && ppro.TickTime && ppro.TickTime.createWithSeconds && ppro.ClipProjectItem && ppro.ClipProjectItem.cast); }
async function derive(args) {
only(args, ["plan", "operationId"]); text(args.operationId, "operationId", 128, /^[A-Za-z0-9._:-]+$/);
const plan = validate(args.plan), project = await ppro.Project.getActiveProject();
if (!project) fail("UXP_NO_ACTIVE_PROJECT", "No active project");
if (!project.lockedAccess || !project.executeTransaction || !project.createSequenceFromMedia) fail("UXP_COMMAND_UNAVAILABLE", "Required sequence and Action APIs are unavailable");
if (await id(project) !== plan.project_guid) fail("UXP_STALE_PROJECT", "The active project changed after preview");
const items = new Map();
for (const segment of plan.segments) items.set(segment.source_project_item_id, await find(project, segment.source_project_item_id));
if (plan.master_audio_project_item_id) items.set(plan.master_audio_project_item_id, await find(project, plan.master_audio_project_item_id));
for (const [itemId, item] of items) { const clip = asClip(item, itemId); if (clip.isMulticamClip && await clip.isMulticamClip()) fail("UXP_UNSUPPORTED_MULTICAM", "Native multicam sources are unsupported; use reviewed ordinary sources"); if (!clip.createSubClipAction) fail("UXP_COMMAND_UNAVAILABLE", "Source cannot create subclips: " + itemId); }
const sequencesBefore = await sequences(project);
if (sequencesBefore.some(function (entry) { return entry.name === plan.sequence_name; })) fail("UXP_NAME_CONFLICT", "The reviewed sequence name already exists");
const target = plan.target_bin_id ? asFolder(await find(project, plan.target_bin_id), "target_bin_id") : undefined;
const parents = new Map(), beforeIds = new Set();
for (const [itemId, item] of items) {
const parent = asFolder(await asClip(item, itemId).getParentBin(), "source parent");
const parentId = await id(parent); parents.set(parentId || itemId, parent);
}
for (const parent of parents.values()) for (const item of Array.from(await parent.getItems() || [])) beforeIds.add(await id(item));
const prefix = plan.sequence_name + " Segment"; let attempted = false;
try {
project.lockedAccess(function () {
const actions = [];
plan.segments.forEach(function (segment, index) {
actions.push(asClip(items.get(segment.source_project_item_id), "source").createSubClipAction(prefix + " V" + (index + 1), tick(segment.source_start_seconds), tick(segment.source_end_seconds), true, plan.mode === "podcast" ? { takeVideo: true, takeAudio: false } : { takeVideo: true, takeAudio: true }));
if (plan.mode === "podcast") actions.push(asClip(items.get(plan.master_audio_project_item_id), "master audio").createSubClipAction(prefix + " A" + (index + 1), tick(segment.master_audio_start_seconds), tick(segment.master_audio_end_seconds), true, { takeVideo: false, takeAudio: true }));
});
attempted = true; commit(project, "Create reviewed dialogue subclips", actions);
});
} catch (error) { if (!attempted) throw error; return partial("dialogue_subclip_transaction_receipt", [], null, []); }
const added = []; for (const parent of parents.values()) for (const item of Array.from(await parent.getItems() || [])) if (!beforeIds.has(await id(item))) added.push(item);
const videos = [], audios = [];
for (let i = 0; i < plan.segments.length; i++) { videos.push(added.find(function (x) { return String(x.name || "") === prefix + " V" + (i + 1); })); if (plan.mode === "podcast") audios.push(added.find(function (x) { return String(x.name || "") === prefix + " A" + (i + 1); })); }
if (videos.some(function (x) { return !x; }) || audios.some(function (x) { return !x; })) return partial("dialogue_subclip_identity_readback", added, null, []);
let sequence; try { sequence = await project.createSequenceFromMedia(plan.sequence_name, [videos[0]], target); } catch (_) { sequence = await addedSequence(project, sequencesBefore); }
if (!sequence) return partial("dialogue_sequence_host_return", added, null, []);
const inserted = [await id(videos[0])];
try {
const editor = ppro.SequenceEditor.getEditor(sequence); if (!editor || !editor.createInsertProjectItemAction) throw new Error("SequenceEditor unavailable");
let offset = 0;
for (let i = 0; i < plan.segments.length; i++) {
if (i) { project.lockedAccess(function () { commit(project, "Insert reviewed dialogue video", [editor.createInsertProjectItemAction(videos[i], tick(offset), 0, 0, false)]); }); inserted.push(await id(videos[i])); }
if (plan.mode === "podcast") { project.lockedAccess(function () { commit(project, "Insert reviewed dialogue audio", [editor.createInsertProjectItemAction(audios[i], tick(offset), 0, 0, false)]); }); inserted.push(await id(audios[i])); }
offset += plan.segments[i].source_end_seconds - plan.segments[i].source_start_seconds;
}
} catch (_) { return partial("dialogue_sequence_partial_insert_receipt", added, sequence, inserted); }
return receipt({ created: true, partial: false, sequence: await snap(sequence), createdSubclips: await snaps(videos.concat(audios)), insertedProjectItemIds: inserted, mode: plan.mode, outputDurationSeconds: plan.output_duration_seconds, originalSourcesChanged: false, renderVerified: false }, "dialogue_sequence_structure_unverified");
}
function validate(value) {
only(value, ["schema_version", "project_guid", "mode", "sequence_name", "target_bin_id", "master_audio_project_item_id", "segments", "output_duration_seconds", "original_sources_unchanged", "render_verified"]);
if (value.schema_version !== 1 || ["talking_head", "podcast"].indexOf(value.mode) < 0) fail("UXP_INVALID_ARGUMENT", "Unsupported dialogue plan");
text(value.project_guid, "project_guid", 512); text(value.sequence_name, "sequence_name", 255);
if (!Array.isArray(value.segments) || !value.segments.length || value.segments.length > 64) fail("UXP_INVALID_ARGUMENT", "segments must contain 1-64 entries");
if (value.original_sources_unchanged !== true || value.render_verified !== false) fail("UXP_INVALID_ARGUMENT", "Original-source and render boundaries are invalid");
if (value.mode === "podcast") text(value.master_audio_project_item_id, "master_audio_project_item_id", 512);
value.segments.forEach(function (s, i) { only(s, ["id", "source_project_item_id", "transcript_revision", "source_start_seconds", "source_end_seconds", "speaker_label", "master_audio_start_seconds", "master_audio_end_seconds"]); text(s.id, "id", 128); text(s.source_project_item_id, "source_project_item_id", 512); const a = secs(s.source_start_seconds), b = secs(s.source_end_seconds); if (b <= a) fail("UXP_INVALID_ARGUMENT", "Segment " + i + " has no duration"); if (value.mode === "podcast") { const c = secs(s.master_audio_start_seconds), d = secs(s.master_audio_end_seconds); if (d <= c || Math.abs((d-c)-(b-a)) > .001) fail("UXP_INVALID_ARGUMENT", "Podcast audio mismatch at segment " + i); } });
return value;
}
async function find(project, wanted) { const queue = [await project.getRootItem()]; let count = 0; while (queue.length) { const item = queue.shift(); if (++count > 4096) fail("UXP_PROJECT_TOO_LARGE", "Project search exceeded 4096 items"); if (await id(item) === wanted) return item; if (item && item.getItems) queue.push.apply(queue, Array.from(await item.getItems() || [])); } fail("UXP_PROJECT_ITEM_NOT_FOUND", "Project item not found: " + wanted); }
async function sequences(project) { const out = []; for (const item of Array.from(await project.getSequences() || [])) out.push(await snap(item)); return out; }
async function addedSequence(project, before) { const known = new Set(before.map(function (x) { return x.id; })); for (const item of Array.from(await project.getSequences() || [])) if (!known.has(await id(item))) return item; return null; }
function commit(project, label, actions) {
// Callers construct actions and invoke this helper inside lockedAccess.
// eslint-disable-next-line @adobe/premierepro/prefer-locked-access-wrapper
const ok = project.executeTransaction(function (compound) { actions.forEach(function (action) { if (!action || compound.addAction(action) === false) fail("UXP_ACTION_REJECTED", "Premiere rejected " + label); }); }, label);
if (!ok) fail("UXP_TRANSACTION_FAILED", "Premiere did not commit " + label);
}
function asClip(item, name) { try { const cast = ppro.ClipProjectItem.cast(item); if (cast) return cast; } catch (_) {} fail("UXP_INVALID_PROJECT_ITEM", name + " is not a clip"); }
function asFolder(item, name) { if (ppro.FolderItem && ppro.FolderItem.cast) try { const cast = ppro.FolderItem.cast(item); if (cast) return cast; } catch (_) {} if (item && item.getItems) return item; fail("UXP_INVALID_PROJECT_ITEM", name + " is not a folder"); }
function tick(value) { return ppro.TickTime.createWithSeconds(secs(value)); }
async function id(item) { if (!item) return ""; if (item.getId) return String(await item.getId()); if (item.getGuid) return String(await item.getGuid()); return ""; }
async function snap(item) { return item ? { id: await id(item), name: String(item.name || "") } : null; }
async function snaps(items) { const out = []; for (const item of items) out.push(await snap(item)); return out; }
function partial(boundary, items, sequence, inserted) { return Promise.all([snaps(items), snap(sequence)]).then(function (x) { return receipt({ created: !!sequence, partial: true, createdSubclips: x[0], sequence: x[1], insertedProjectItemIds: inserted, originalSourcesChanged: false, renderVerified: false }, boundary); }); }
function receipt(values, boundary) { return Object.assign({ outcome: "committed_unverified", verified: false, verificationBoundary: boundary }, values); }
function only(value, keys) { if (!value || typeof value !== "object" || Array.isArray(value)) fail("UXP_INVALID_ARGUMENT", "Arguments must be an object"); Object.keys(value).forEach(function (key) { if (keys.indexOf(key) < 0) fail("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }); }
function text(value, name, max, pattern) { if (typeof value !== "string" || !value || value.length > max || (pattern && !pattern.test(value))) fail("UXP_INVALID_ARGUMENT", name + " is invalid"); return value; }
function secs(value) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 86400) fail("UXP_INVALID_ARGUMENT", "Time must be 0-86400 seconds"); return value; }
function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
}
return { createDialogueWorkflowDefinitions: createDialogueWorkflowDefinitions };
});
@@ -0,0 +1,229 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpEffectParameterCatalogWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// A caller can already inspect a known parameter through the automation
// workflow. This catalog fills the discovery gap without returning parameter
// values, which may be arbitrary PointF/Color objects or user content.
function createEffectParameterCatalogWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const MAX_TRACK_ITEMS = 512;
const MAX_COMPONENTS = 512;
const MAX_PARAMETERS = 64;
const definitions = {
"parameters.catalog.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canInspectParameterCatalog,
handler: inspectParameterCatalog
}
};
function canInspectParameterCatalog() {
return !!(ppro && ppro.Project && typeof ppro.Project.getActiveProject === "function");
}
async function inspectParameterCatalog(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "componentIndex", "expectedSequenceGuid", "expectedComponentId"]);
const input = coordinateInput(args);
const first = await catalogSnapshot(input);
assertExpected(input.expectedSequenceGuid, first.sequenceGuid, "UXP_STALE_PARAMETER_CATALOG", "Active sequence GUID");
assertExpected(input.expectedComponentId, first.component.id, "UXP_STALE_PARAMETER_CATALOG", "Component identity");
const second = await catalogSnapshot(input);
if (!sameSnapshot(first, second)) {
throw commandError("UXP_STALE_PARAMETER_CATALOG", "The project, active sequence, component, or parameter descriptors changed while being inspected; retry the catalog inspection");
}
return {
projectGuid: first.projectGuid,
sequenceGuid: first.sequenceGuid,
mediaType: input.mediaType,
trackIndex: input.trackIndex,
clipIndex: input.clipIndex,
componentIndex: input.componentIndex,
component: first.component,
parameterCount: first.parameters.length,
parameters: first.parameters,
verificationBoundary: "bounded_effect_parameter_catalog_double_readback"
};
}
function coordinateInput(args) {
return {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: boundedIndex(args.trackIndex, "trackIndex"),
clipIndex: boundedIndex(args.clipIndex, "clipIndex"),
componentIndex: boundedIndex(args.componentIndex, "componentIndex"),
expectedSequenceGuid: args.expectedSequenceGuid == null ? null : requestedGuid(args.expectedSequenceGuid, "expectedSequenceGuid"),
expectedComponentId: args.expectedComponentId == null ? null : boundedText(args.expectedComponentId, "expectedComponentId", 512, false)
};
}
async function catalogSnapshot(input) {
const project = await activeProject();
const projectGuid = requiredGuid(project.guid, "active project GUID");
if (typeof project.getActiveSequence !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose the active sequence for parameter catalog inspection");
}
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
const sequenceGuid = requiredGuid(sequence.guid, "active sequence GUID");
const item = await trackItemAt(sequence, input.mediaType, input.trackIndex, input.clipIndex);
const component = await componentAt(item, input.componentIndex);
const componentSnapshot = await componentDescriptor(component);
const parameterCount = boundedCount(component.getParamCount && component.getParamCount(), "component parameter count", MAX_PARAMETERS);
if (typeof component.getParam !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose component parameter lookup");
}
const parameters = [];
for (let index = 0; index < parameterCount; index += 1) {
parameters.push(await parameterDescriptor(component.getParam(index), index));
}
return { projectGuid, sequenceGuid, component: componentSnapshot, parameters };
}
async function activeProject() {
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
return project;
}
async function trackItemAt(sequence, mediaType, trackIndex, clipIndex) {
const countMethod = mediaType === "video" ? "getVideoTrackCount" : "getAudioTrackCount";
const trackMethod = mediaType === "video" ? "getVideoTrack" : "getAudioTrack";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented " + mediaType + " track access");
}
const count = boundedCount(await sequence[countMethod](), mediaType + " track count", MAX_TRACK_ITEMS);
if (trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", "trackIndex is out of range");
const track = await sequence[trackMethod](trackIndex);
const itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented clip-item access on the requested track");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (items.length > MAX_TRACK_ITEMS) {
throw commandError("UXP_TARGET_TOO_LARGE", "The requested track has more than " + MAX_TRACK_ITEMS + " clip items");
}
if (!items[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
return items[clipIndex];
}
async function componentAt(item, componentIndex) {
if (!item || typeof item.getComponentChain !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "The requested clip does not expose a documented component chain");
}
const chain = await item.getComponentChain();
if (!chain || typeof chain.getComponentCount !== "function" || typeof chain.getComponentAtIndex !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a documented component chain");
}
const count = boundedCount(chain.getComponentCount(), "component count", MAX_COMPONENTS);
if (componentIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", "componentIndex is out of range");
const component = chain.getComponentAtIndex(componentIndex);
if (!component) throw commandError("UXP_TARGET_NOT_FOUND", "Premiere did not return the requested component");
return component;
}
async function componentDescriptor(component) {
if (typeof component.getMatchName !== "function" || typeof component.getDisplayName !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented component identity getters");
}
const matchName = boundedText(await component.getMatchName(), "component match name", 512, true);
const displayName = boundedText(await component.getDisplayName(), "component display name", 512, true);
const id = matchName || displayName;
if (!id) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned a component without a match or display name");
return { id, matchName, displayName };
}
async function parameterDescriptor(parameter, index) {
if (!parameter || typeof parameter.displayName !== "string" || typeof parameter.areKeyframesSupported !== "function" || typeof parameter.isTimeVarying !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not expose documented parameter descriptor access");
}
const displayName = boundedText(parameter.displayName, "parameter display name", 512, true);
const keyframesSupported = await parameter.areKeyframesSupported();
const timeVarying = parameter.isTimeVarying();
if (typeof keyframesSupported !== "boolean" || typeof timeVarying !== "boolean") {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid parameter animation descriptor");
}
return { index, displayName, keyframesSupported, timeVarying };
}
function sameSnapshot(left, right) {
if (!left || !right || left.projectGuid !== right.projectGuid || left.sequenceGuid !== right.sequenceGuid ||
left.component.id !== right.component.id || left.component.matchName !== right.component.matchName ||
left.component.displayName !== right.component.displayName || left.parameters.length !== right.parameters.length) return false;
return left.parameters.every(function (parameter, index) {
const other = right.parameters[index];
return other && parameter.index === other.index && parameter.displayName === other.displayName &&
parameter.keyframesSupported === other.keyframesSupported && parameter.timeVarying === other.timeVarying;
});
}
return definitions;
}
function assertOnlyKeys(value, allowed) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "arguments must be an object");
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
}
function enumValue(value, name, values) {
if (typeof value !== "string" || !values.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be " + values.join(" or "));
return value;
}
function boundedIndex(value, name) {
if (!Number.isInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 through 511");
return value;
}
function boundedCount(value, name, maximum) {
const count = Number(value);
if (!Number.isInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name);
if (count > maximum) throw commandError("UXP_TARGET_TOO_LARGE", "The " + name + " exceeds the " + maximum + " entry limit");
return count;
}
function requiredGuid(value, name) {
const guid = boundedGuid(value, name);
if (!guid) throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded non-empty GUID");
return guid;
}
function boundedGuid(value, name) {
let text = "";
try { text = String(value && typeof value.toString === "function" ? value.toString() : value || ""); } catch (_) {}
if (!text || text.length > 512) throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded non-empty GUID");
return text;
}
function requestedGuid(value, name) {
let text = "";
try { text = String(value && typeof value.toString === "function" ? value.toString() : value || ""); } catch (_) {}
if (!text || text.length > 512) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a bounded non-empty GUID");
return text;
}
function boundedText(value, name, maximum, allowEmpty) {
if (typeof value !== "string" || value.length > maximum || (!allowEmpty && !value)) {
throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded " + (allowEmpty ? "string" : "non-empty string"));
}
return value;
}
function assertExpected(expected, actual, code, name) {
if (expected != null && expected !== actual) throw commandError(code, name + " differs from the inspected value; retry the catalog inspection");
}
function commandError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { createEffectParameterCatalogWorkflowDefinitions };
});
+405
View File
@@ -0,0 +1,405 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpEvents = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const DEFAULT_CAPACITY = 512;
const MAX_CAPACITY = 2048;
const MAX_WAIT_MS = 60000;
const MAX_RESULTS = 256;
const MAX_ENCODE_JOBS = 64;
const SAFE_DETAIL_KEYS = new Set([
"state", "progress", "phase", "trackIndex", "mediaType", "attributed", "jobId"
]);
// Snap events are notifications only. Keep their host constants and the
// public journal names in one small, testable mapping so neither event
// payloads nor any unrecognized host event can cross the bridge.
function createTimelineSnapEventDefinitions(snapEvent) {
if (!snapEvent || typeof snapEvent !== "object") return [];
return [
["timeline.snap.keyframe", snapEvent.EVENT_SNAP_TO_KEYFRAME],
["timeline.snap.trackItem", snapEvent.EVENT_SNAP_TO_TRACKITEM],
["timeline.snap.guides", snapEvent.EVENT_SNAP_TO_GUIDES],
["timeline.snap.razor.playhead", snapEvent.EVENT_SNAP_RAZOR_TO_PLAYHEAD],
["timeline.snap.razor.marker", snapEvent.EVENT_SNAP_RAZOR_TO_MARKER],
["timeline.snap.playhead.trackItemEdge", snapEvent.EVENT_SNAP_PLAYHEAD_TO_TRACKITEM_EDGE]
].filter(function (entry) {
return typeof entry[1] === "string" && entry[1].length > 0;
}).map(function (entry) {
return {
category: "timeline",
name: entry[0],
eventName: entry[1],
stateInvalidating: false,
coalesceKey: null
};
});
}
// These two root OperationCompleteEvent constants are not completion
// attestations: one reports a clip's extend limit and the other reports an
// effect drag-over. Keep them passive and redact the native event object.
// Drag-over may be noisy, so retain only the most recent consecutive receipt.
function createOperationBoundaryEventDefinitions(operationCompleteEvent) {
if (!operationCompleteEvent || typeof operationCompleteEvent !== "object") return [];
return [
["operation.clip.extend.reached", operationCompleteEvent.EVENT_CLIP_EXTEND_REACHED, null],
["operation.effect.drag.over", operationCompleteEvent.EVENT_EFFECT_DRAG_OVER, "operation.effect.drag.over"]
].filter(function (entry) {
return typeof entry[1] === "string" && entry[1].length > 0;
}).map(function (entry) {
return {
category: "operation",
name: entry[0],
eventName: entry[1],
stateInvalidating: false,
coalesceKey: entry[2]
};
});
}
function createEventJournal(options) {
const value = options || {};
const capacity = boundedInteger(value.capacity == null ? DEFAULT_CAPACITY : value.capacity, "capacity", 16, MAX_CAPACITY);
const now = typeof value.now === "function" ? value.now : function () { return Date.now(); };
const setTimer = typeof value.setTimer === "function" ? value.setTimer : setTimeout;
const clearTimer = typeof value.clearTimer === "function" ? value.clearTimer : clearTimeout;
const entries = [];
const waiters = new Set();
const jobs = new Map();
const jobWaiters = new Set();
let revision = 0;
let dropped = 0;
let evictedThroughRevision = 0;
let jobSequence = 0;
let unattributedEncoderEvents = 0;
let closed = false;
function append(input) {
if (closed) return null;
const event = normalizeEvent(input, ++revision, now());
const previous = entries.length ? entries[entries.length - 1] : null;
if (event.coalesceKey && previous && previous.coalesceKey === event.coalesceKey) {
event.coalesced = (previous.coalesced || 0) + 1;
entries[entries.length - 1] = event;
} else {
entries.push(event);
}
while (entries.length > capacity) {
const evicted = entries.shift();
if (evicted) evictedThroughRevision = Math.max(evictedThroughRevision, evicted.revision);
dropped += 1;
}
settleWaiters();
return publicEvent(event);
}
function recordHostEvent(input) {
const value = Object.assign({}, input, { detail: Object.assign({}, input && input.detail) });
let attributedJob = null;
if (value.category === "encoder") {
const candidates = Array.from(jobs.values()).filter(function (job) { return !job.terminal; });
if (candidates.length === 1) {
attributedJob = candidates[0];
value.detail.attributed = true;
value.detail.jobId = attributedJob.jobId;
} else {
value.detail.attributed = false;
unattributedEncoderEvents += 1;
}
}
const receipt = append(value);
if (attributedJob && receipt) applyEncoderReceipt(attributedJob, receipt);
return receipt;
}
function list(input) {
const query = normalizeQuery(input);
const oldestRevision = entries.length ? entries[0].revision : revision + 1;
const overflow = query.afterRevision > 0 && query.afterRevision < evictedThroughRevision;
const matching = entries.filter(function (event) {
return event.revision > query.afterRevision && matches(event, query);
}).slice(0, query.limit).map(publicEvent);
return {
latestRevision: revision,
oldestRevision,
dropped,
overflow,
timedOut: false,
events: matching
};
}
function wait(input) {
const query = normalizeQuery(input);
const immediate = list(query);
if (immediate.events.length || immediate.overflow || query.timeoutMs === 0 || closed) {
return Promise.resolve(Object.assign(immediate, { closed }));
}
return new Promise(function (resolve) {
const waiter = { query, resolve, timer: null };
waiter.timer = setTimer(function () {
waiters.delete(waiter);
resolve(Object.assign(list(query), { timedOut: true, closed }));
}, query.timeoutMs);
waiters.add(waiter);
});
}
function beginEncodeJob(input) {
if (closed) throw trackerError("UXP_ENCODE_TRACKER_CLOSED", "Encode receipt tracking is closed");
const value = input || {};
const operationId = value.operationId == null ? null : boundedToken(value.operationId, "operationId");
const jobId = operationId || ("encode-" + now() + "-" + (++jobSequence));
if (jobs.has(jobId)) return publicJob(jobs.get(jobId));
pruneJobs();
if (jobs.size >= MAX_ENCODE_JOBS) throw trackerError("UXP_ENCODE_TRACKER_FULL", "Encode receipt tracker is full");
const timestamp = new Date(now()).toISOString();
const job = {
jobId,
operationId,
kind: boundedToken(value.kind || "unknown", "kind"),
state: "submitting",
progress: null,
hostEvent: null,
eventRevision: null,
terminal: false,
terminalReason: null,
createdAt: timestamp,
updatedAt: timestamp
};
jobs.set(jobId, job);
return publicJob(job);
}
function markEncodeAccepted(jobId) {
const job = requiredJob(jobId);
if (job.state === "submitting") updateJob(job, { state: "accepted" });
return publicJob(job);
}
function markEncodeRejected(jobId, reason) {
const job = requiredJob(jobId);
updateJob(job, {
state: "failed",
terminal: true,
terminalReason: boundedToken(reason || "host_rejected", "reason")
});
return publicJob(job);
}
function listEncodeJobs(input) {
const value = input || {};
const jobId = value.jobId == null ? null : boundedToken(value.jobId, "jobId");
const limit = boundedInteger(value.limit == null ? 32 : value.limit, "limit", 1, MAX_ENCODE_JOBS);
const values = jobId ? [requiredJob(jobId)] : Array.from(jobs.values()).slice(-limit).reverse();
return {
jobs: values.map(publicJob),
count: values.length,
unattributedEncoderEvents,
correlation: "single-active-job-only"
};
}
function waitForEncodeJob(input) {
const value = input || {};
const job = requiredJob(value.jobId);
const timeoutMs = boundedInteger(value.timeoutMs == null ? 0 : value.timeoutMs, "timeoutMs", 0, MAX_WAIT_MS);
if (job.terminal || timeoutMs === 0 || closed) {
return Promise.resolve({ job: publicJob(job), timedOut: false, closed });
}
return new Promise(function (resolve) {
const waiter = { jobId: job.jobId, resolve, timer: null };
waiter.timer = setTimer(function () {
jobWaiters.delete(waiter);
resolve({ job: publicJob(requiredJob(waiter.jobId)), timedOut: true, closed });
}, timeoutMs);
jobWaiters.add(waiter);
});
}
function settleWaiters() {
for (const waiter of Array.from(waiters)) {
const result = list(waiter.query);
if (!result.events.length && !result.overflow) continue;
waiters.delete(waiter);
if (waiter.timer) clearTimer(waiter.timer);
waiter.resolve(Object.assign(result, { closed: false }));
}
}
function close() {
if (closed) return;
closed = true;
for (const waiter of Array.from(waiters)) {
waiters.delete(waiter);
if (waiter.timer) clearTimer(waiter.timer);
waiter.resolve(Object.assign(list(waiter.query), { closed: true }));
}
for (const waiter of Array.from(jobWaiters)) {
jobWaiters.delete(waiter);
if (waiter.timer) clearTimer(waiter.timer);
waiter.resolve({ job: publicJob(requiredJob(waiter.jobId)), timedOut: false, closed: true });
}
}
function status() {
return {
capacity,
size: entries.length,
latestRevision: revision,
oldestRevision: entries.length ? entries[0].revision : revision + 1,
dropped,
encodeJobs: jobs.size,
unattributedEncoderEvents,
closed
};
}
function applyEncoderReceipt(job, receipt) {
const states = {
"encoder.queued": { state: "queued" },
"encoder.progress": { state: "rendering", progress: receipt.detail.progress == null ? job.progress : receipt.detail.progress },
"encoder.complete": { state: "completed", progress: 1, terminal: true, terminalReason: "completed" },
"encoder.error": { state: "failed", terminal: true, terminalReason: "encoder_error" },
"encoder.cancelled": { state: "cancelled", terminal: true, terminalReason: "cancelled" }
};
updateJob(job, Object.assign({ hostEvent: receipt.name, eventRevision: receipt.revision }, states[receipt.name] || {}));
}
function updateJob(job, values) {
Object.assign(job, values, { updatedAt: new Date(now()).toISOString() });
if (!job.terminal) return;
for (const waiter of Array.from(jobWaiters)) {
if (waiter.jobId !== job.jobId) continue;
jobWaiters.delete(waiter);
if (waiter.timer) clearTimer(waiter.timer);
waiter.resolve({ job: publicJob(job), timedOut: false, closed });
}
}
function requiredJob(jobId) {
const id = boundedToken(jobId, "jobId");
const job = jobs.get(id);
if (!job) throw trackerError("UXP_ENCODE_JOB_NOT_FOUND", "Encode job receipt was not found");
return job;
}
function pruneJobs() {
if (jobs.size < MAX_ENCODE_JOBS) return;
const terminal = Array.from(jobs.values()).filter(function (job) { return job.terminal; });
if (terminal.length) jobs.delete(terminal[0].jobId);
}
return {
append, recordHostEvent, list, wait, close, status,
beginEncodeJob, markEncodeAccepted, markEncodeRejected, listEncodeJobs, waitForEncodeJob
};
}
function publicJob(job) {
return {
jobId: job.jobId,
operationId: job.operationId,
kind: job.kind,
state: job.state,
progress: job.progress,
hostEvent: job.hostEvent,
eventRevision: job.eventRevision,
terminal: job.terminal,
terminalReason: job.terminalReason,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
verificationBoundary: job.terminal ? "encoder_terminal_event_only" : "encoder_host_acceptance_or_event"
};
}
function trackerError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function normalizeEvent(input, revision, timestamp) {
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("event must be an object");
const category = boundedToken(input.category, "category");
const name = boundedToken(input.name, "name");
const coalesceKey = input.coalesceKey == null ? null : boundedToken(input.coalesceKey, "coalesceKey");
return {
revision,
category,
name,
receivedAt: new Date(timestamp).toISOString(),
detail: safeDetail(input.detail),
coalesceKey,
coalesced: 0
};
}
function normalizeQuery(input) {
const value = input || {};
if (typeof value !== "object" || Array.isArray(value)) throw new Error("event query must be an object");
return {
afterRevision: boundedInteger(value.afterRevision == null ? 0 : value.afterRevision, "afterRevision", 0, Number.MAX_SAFE_INTEGER),
categories: tokenList(value.categories, "categories"),
eventNames: tokenList(value.eventNames, "eventNames"),
limit: boundedInteger(value.limit == null ? 100 : value.limit, "limit", 1, MAX_RESULTS),
timeoutMs: boundedInteger(value.timeoutMs == null ? 0 : value.timeoutMs, "timeoutMs", 0, MAX_WAIT_MS)
};
}
function matches(event, query) {
return (!query.categories.length || query.categories.indexOf(event.category) !== -1)
&& (!query.eventNames.length || query.eventNames.indexOf(event.name) !== -1);
}
function safeDetail(value) {
if (value == null) return {};
if (typeof value !== "object" || Array.isArray(value)) throw new Error("event detail must be an object");
const result = {};
for (const key of Object.keys(value)) {
if (!SAFE_DETAIL_KEYS.has(key)) continue;
const item = value[key];
if (typeof item === "boolean" || (typeof item === "number" && Number.isFinite(item))) result[key] = item;
else if (typeof item === "string") result[key] = item.slice(0, 128);
}
return result;
}
function tokenList(value, name) {
if (value == null) return [];
if (!Array.isArray(value) || value.length > 32) throw new Error(name + " must be an array of at most 32 tokens");
return value.map(function (item) { return boundedToken(item, name); });
}
function boundedToken(value, name) {
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
throw new Error(name + " must be a 1-128 character token");
}
return value;
}
function boundedInteger(value, name, minimum, maximum) {
const number = Number(value);
if (!Number.isInteger(number) || number < minimum || number > maximum) {
throw new Error(name + " must be an integer between " + minimum + " and " + maximum);
}
return number;
}
function publicEvent(event) {
return {
revision: event.revision,
category: event.category,
name: event.name,
receivedAt: event.receivedAt,
detail: Object.assign({}, event.detail),
coalesced: event.coalesced || 0
};
}
return { createEventJournal, createTimelineSnapEventDefinitions, createOperationBoundaryEventDefinitions };
});
+176
View File
@@ -0,0 +1,176 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpHybridBenchmark = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const WORKLOAD_ID = "weighted-energy-v1";
const DEFAULT_ADDON = "premiere-mcp-benchmark.uxpaddon";
function run(options) {
const input = options || {};
assertOnlyKeys(input, [
"sampleCount", "warmupCount", "iterations", "inputLength", "seed",
"addonName", "loadAddon", "requireAddon", "now", "readMemory"
]);
const sampleCount = boundedInteger(input.sampleCount == null ? 30 : input.sampleCount, "sampleCount", 5, 200);
const warmupCount = boundedInteger(input.warmupCount == null ? 3 : input.warmupCount, "warmupCount", 0, 20);
const iterations = boundedInteger(input.iterations == null ? 4 : input.iterations, "iterations", 1, 50);
const inputLength = boundedInteger(input.inputLength == null ? 131072 : input.inputLength, "inputLength", 1024, 1048576);
const seed = boundedInteger(input.seed == null ? 1337 : input.seed, "seed", 1, 2147483646);
const now = typeof input.now === "function" ? input.now : monotonicNow;
const readMemory = typeof input.readMemory === "function" ? input.readMemory : defaultMemoryReader;
const data = deterministicInput(inputLength, seed);
const js = measure(function () { return weightedEnergy(data, iterations); }, sampleCount, warmupCount, now, readMemory);
const addon = loadAddon(input);
let native = null, checksumMatch = null;
if (addon.loaded) {
if (!addon.value || typeof addon.value.runBenchmarkKernel !== "function") {
throw benchmarkError("UXP_HYBRID_CONTRACT_MISMATCH", "The addon must export runBenchmarkKernel(Float64Array, iterations)");
}
native = measure(function () {
const value = Number(addon.value.runBenchmarkKernel(data, iterations));
if (!Number.isFinite(value)) throw benchmarkError("UXP_HYBRID_INVALID_RESULT", "The native benchmark returned a non-finite checksum");
return value;
}, sampleCount, warmupCount, now, readMemory);
checksumMatch = sameChecksum(js.checksum, native.checksum);
}
if (input.requireAddon === true && !addon.loaded) {
throw benchmarkError("UXP_HYBRID_ADDON_REQUIRED", addon.error || "The benchmark addon was not available");
}
return {
schemaVersion: 1,
workloadId: WORKLOAD_ID,
configuration: { sampleCount, warmupCount, iterations, inputLength, seed },
addon: { name: addon.name, loaded: addon.loaded, error: addon.error },
javascript: js,
native,
checksumMatch,
promotionEligible: false,
promotionBoundary: "Run the cross-platform evidence verifier; this local result alone cannot enable a hybrid addon."
};
}
function weightedEnergy(values, iterations) {
let checksum = 0;
for (let pass = 0; pass < iterations; pass += 1) {
let energy = 0, crossings = 0, previous = values[0];
for (let i = 0; i < values.length; i += 1) {
const sample = values[i];
energy += sample * sample * ((i & 15) + 1);
if ((sample < 0) !== (previous < 0)) crossings += 1;
previous = sample;
}
checksum += energy / values.length + crossings * 0.000001 + pass * 0.000000001;
}
return checksum;
}
function deterministicInput(length, seed) {
const values = new Float64Array(length);
let state = seed;
for (let i = 0; i < length; i += 1) {
state = state * 16807 % 2147483647;
values[i] = state / 1073741823.5 - 1;
}
return values;
}
function measure(workload, sampleCount, warmupCount, now, readMemory) {
for (let i = 0; i < warmupCount; i += 1) workload();
const beforeBytes = safeMemory(readMemory), samples = [];
let checksum = null;
for (let i = 0; i < sampleCount; i += 1) {
const startedAt = Number(now());
checksum = Number(workload());
const finishedAt = Number(now());
const elapsed = finishedAt - startedAt;
if (!Number.isFinite(elapsed) || elapsed < 0) throw benchmarkError("UXP_HYBRID_INVALID_CLOCK", "Benchmark clock must be monotonic");
samples.push(elapsed);
}
const afterBytes = safeMemory(readMemory);
return {
sampleCount,
p50Ms: percentile(samples, 0.5),
p95Ms: percentile(samples, 0.95),
minMs: Math.min.apply(Math, samples),
maxMs: Math.max.apply(Math, samples),
checksum,
memory: {
beforeBytes,
afterBytes,
deltaBytes: beforeBytes == null || afterBytes == null ? null : afterBytes - beforeBytes,
boundary: "runtime_heap_snapshot_if_available"
}
};
}
function loadAddon(options) {
const name = typeof options.addonName === "string" && options.addonName
? options.addonName
: DEFAULT_ADDON;
if (!/^[A-Za-z0-9._-]{1,128}\.uxpaddon$/.test(name)) {
return { name, loaded: false, value: null, error: "addonName must be a simple .uxpaddon filename" };
}
const loader = typeof options.loadAddon === "function" ? options.loadAddon : defaultAddonLoader;
try { return { name, loaded: true, value: loader(name), error: null }; }
catch (error) { return { name, loaded: false, value: null, error: error && error.message || String(error) }; }
}
function defaultAddonLoader(name) {
if (typeof require !== "function") throw new Error("CommonJS addon loading is unavailable");
return require(name);
}
function monotonicNow() {
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
return Date.now();
}
function defaultMemoryReader() {
if (typeof performance !== "undefined" && performance.memory && Number.isFinite(performance.memory.usedJSHeapSize)) {
return performance.memory.usedJSHeapSize;
}
return null;
}
function safeMemory(reader) {
try {
const value = reader();
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
} catch (_) { return null; }
}
function percentile(values, quantile) {
const sorted = values.slice().sort(function (left, right) { return left - right; });
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * quantile) - 1));
return sorted[index];
}
function sameChecksum(left, right) {
return Number.isFinite(left) && Number.isFinite(right) &&
Math.abs(left - right) <= Math.max(1e-9, Math.abs(left) * 1e-10);
}
function boundedInteger(value, name, minimum, maximum) {
const number = Number(value);
if (!Number.isInteger(number) || number < minimum || number > maximum) {
throw benchmarkError("UXP_HYBRID_INVALID_ARGUMENT", name + " must be an integer between " + minimum + " and " + maximum);
}
return number;
}
function assertOnlyKeys(value, allowed) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw benchmarkError("UXP_HYBRID_INVALID_ARGUMENT", "options must be an object");
for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw benchmarkError("UXP_HYBRID_INVALID_ARGUMENT", "Unexpected option: " + key);
}
function benchmarkError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { WORKLOAD_ID, run, weightedEnergy, deterministicInput, percentile, sameChecksum };
});
+582
View File
@@ -0,0 +1,582 @@
"use strict";
const { entrypoints, host, storage } = require("uxp");
const ppro = require("premierepro");
const Protocol = globalThis.PremiereMcpProtocol;
const TranscriptSupport = globalThis.PremiereMcpTranscript;
const TranscriptImport = globalThis.PremiereMcpTranscriptImport || (typeof require === "function" ? require("./transcript-import.cjs") : null);
const WorkspaceSupport = globalThis.PremiereMcpWorkspace;
const EventSupport = globalThis.PremiereMcpEvents;
const Commands = globalThis.PremiereMcpCommands;
const workspaceBroker = WorkspaceSupport.createWorkspaceBroker({ fs: storage && storage.localFileSystem });
const eventJournal = EventSupport.createEventJournal({ capacity: 512 });
const transcriptImportRuntime = TranscriptImport && typeof TranscriptImport.createTranscriptImportRuntime === "function"
? TranscriptImport.createTranscriptImportRuntime({ ppro, TranscriptSupport, Protocol })
: null;
const commandRegistry = Commands.createCommandRegistry({
ppro,
Protocol,
workspace: workspaceBroker,
events: eventJournal,
storage: typeof globalThis !== "undefined" ? globalThis.localStorage : null,
transcriptImportHandler: transcriptImportRuntime && transcriptImportRuntime.importTranscript,
transcriptImportProbe: transcriptImportRuntime && transcriptImportRuntime.canImportTranscript
});
let socket = null;
let reconnectTimer = null;
let lastState = "";
let stateRevision = 0;
let fallbackPollTimer = null;
const operationTracker = Protocol.createOperationTracker();
const eventSubscriptions = [];
const targetEventSubscriptions = [];
let targetSubscriptionGeneration = 0;
entrypoints.setup({
panels: {
mcpBridgePanel: {
create() { subscribeHostEvents(); },
show() { publishState("panel.show"); },
destroy() { unsubscribeHostEvents(); stopFallbackPolling(); eventJournal.close(); void commandRegistry.dispose(); disconnect(); }
}
}
});
document.addEventListener("DOMContentLoaded", async () => {
document.getElementById("connect").addEventListener("click", connect);
document.getElementById("refresh").addEventListener("click", () => publishState("manual"));
document.getElementById("choose-workspace").addEventListener("click", chooseWorkspace);
document.getElementById("revoke-workspace").addEventListener("click", revokeWorkspace);
try { await workspaceBroker.initialize(); } catch (error) { setStatus(error.message || String(error)); }
try { await commandRegistry.initialize(); } catch (error) { setStatus(error.message || String(error)); }
renderWorkspaceStatus();
subscribeHostEvents();
connect();
startFallbackPolling();
});
async function capabilities() {
const value = await commandRegistry.capabilities();
value.commands["capabilities.get"].cancellable = false;
value.commands["state.get"].cancellable = false;
value.commands["operation.cancel"] = { supported: true, readOnly: false, scope: "preflight only" };
if (value.commands["frame.export"]) Object.assign(value.commands["frame.export"], {
cancellable: "preflight only", verification: "exporter return value", undoable: false, atomic: false
});
const supportedHost = TranscriptSupport.versionAtLeast(host && host.version, "25.6.0");
const transcriptImportHost = TranscriptSupport.versionAtLeast(host && host.version, "26.3.0");
const transcriptExportApi = supportedHost && !!(ppro.Transcript && ppro.Transcript.exportToJSON && ppro.Transcript.importFromJSON);
const transcriptNativeHasApi = supportedHost && !!(ppro.Transcript && typeof ppro.Transcript.hasTranscript === "function");
const transcriptHasApi = transcriptNativeHasApi || !!(supportedHost && ppro.Transcript && typeof ppro.Transcript.exportToJSON === "function");
Object.assign(value.commands, {
"transcript.export": { supported: transcriptExportApi, readOnly: true, minVersion: "25.6.0" },
"transcript.search": { supported: transcriptExportApi, readOnly: true, minVersion: "25.6.0" },
"transcript.has": {
supported: transcriptHasApi, readOnly: true, minVersion: transcriptNativeHasApi ? "26.3.0" : "25.6.0",
nativeCheckMinVersion: "26.3.0", nativeCheck: transcriptNativeHasApi,
fallback: transcriptNativeHasApi ? null : transcriptHasApi ? "export-probe" : null,
fallbackMinVersion: transcriptHasApi ? "25.6.0" : null
},
"transcript.import": {
supported: transcriptImportHost && !!transcriptImportRuntime && transcriptImportRuntime.canImportTranscript(),
readOnly: false,
destructive: true,
undoable: true,
idempotent: true,
minVersion: "26.3.0",
confirmationRequired: true,
verification: "bounded exact transcript-export SHA-256 readback"
},
"captions.inspect": { supported: supportedHost, readOnly: true, minVersion: "25.6.0" },
"captions.create": { supported: false, reason: "No documented Premiere UXP caption creation API." },
"captions.update": { supported: false, reason: "No documented Premiere UXP caption text/timing mutation API." },
"captions.delete": { supported: false, reason: "No documented Premiere UXP caption deletion API." }
});
value.hostVersion = host && host.version || null;
Object.assign(value, {
events: {
host: supportedHostEvents().map((event) => event.name),
stateNotifications: true,
journal: eventJournal.status(),
fallbackPolling: { enabled: true, intervalMs: 5000 },
operationLifecycle: ["started", "progress", "completed", "failed", "cancelled"]
},
operationSemantics: {
mutations: "Only action-based commands executed by Project.executeTransaction may claim an undo boundary.",
rollback: "No atomic rollback is claimed. Callers must inspect result verification metadata.",
cancellation: "Cooperative before a non-cancellable Premiere host call; no interruption is claimed after that boundary."
},
fallback: { backend: "cep", reason: "Use CEP/QE only when a command is absent or reports unsupported; never silently retry a failed UXP mutation." }
});
return value;
}
function castClipProjectItem(item) {
try {
const clip = ppro.ClipProjectItem.cast(item);
if (clip) return clip;
} catch (_) {}
throw new Error("The resolved project item is not a media clip");
}
async function selectedClipProjectItem(project) {
if (!ppro.ProjectUtils || typeof ppro.ProjectUtils.getSelection !== "function") {
throw new Error("Project panel selection is unavailable; pass projectItemId or projectItemName");
}
const selection = await ppro.ProjectUtils.getSelection(project);
const items = selection && await selection.getItems();
if (!items || items.length !== 1) throw new Error("Select exactly one media project item, or pass projectItemId/projectItemName");
return castClipProjectItem(items[0]);
}
async function findProjectItem(project, args) {
const wantedId = args && args.projectItemId != null ? String(args.projectItemId) : "";
const wantedName = args && args.projectItemName != null ? String(args.projectItemName) : "";
if (!wantedId && !wantedName) return selectedClipProjectItem(project);
const queue = [await project.getRootItem()];
while (queue.length) {
const folder = queue.shift();
const children = await folder.getItems();
for (let i = 0; i < children.length; i += 1) {
const item = children[i];
const itemId = typeof item.getId === "function" ? String(item.getId()) : "";
const candidate = TranscriptSupport.matchingClipCandidate(
item, itemId, wantedId, wantedName, castClipProjectItem
);
if (candidate.clip) return candidate.clip;
try {
const childFolder = ppro.FolderItem.cast(item);
if (childFolder) queue.push(childFolder);
} catch (_) {}
}
}
throw new Error("Project item not found");
}
async function transcriptContext(args) {
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
if (!ppro.Transcript || typeof ppro.Transcript.exportToJSON !== "function") throw new Error("Transcript APIs require Premiere Pro 25.6 or newer");
const clip = await findProjectItem(project, args || {});
const projectItem = ppro.ProjectItem.cast(clip);
return {
project,
projectGuid: String(project.guid),
clip,
projectItemId: String(await projectItem.getId()),
projectItemName: clip.name
};
}
async function exportTranscript(args) {
const context = await transcriptContext(args);
const json = await ppro.Transcript.exportToJSON(context.clip);
if (typeof json !== "string" || !json) throw new Error("The selected clip has no transcript");
TranscriptSupport.parseTranscriptJSON(json);
return {
projectGuid: context.projectGuid,
projectItemId: context.projectItemId,
projectItemName: context.projectItemName,
json
};
}
async function searchTranscript(args) {
const exported = await exportTranscript(args);
const result = TranscriptSupport.searchTranscriptJSON(exported.json, args.query, {
caseSensitive: args.caseSensitive, maxResults: args.maxResults
});
return Object.assign({ projectItemId: exported.projectItemId, projectItemName: exported.projectItemName }, result);
}
async function hasTranscript(args) {
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
if (!ppro.Transcript) throw new Error("Transcript APIs require Premiere Pro 25.6 or newer");
const clip = await findProjectItem(project, args || {});
const projectItem = ppro.ProjectItem.cast(clip);
const context = {
projectGuid: String(project.guid),
projectItemId: String(await projectItem.getId()),
projectItemName: clip.name,
clip
};
if (typeof ppro.Transcript.hasTranscript === "function") {
return {
projectGuid: context.projectGuid,
projectItemId: context.projectItemId,
projectItemName: context.projectItemName,
hasTranscript: !!await ppro.Transcript.hasTranscript(context.clip),
method: "native"
};
}
if (typeof ppro.Transcript.exportToJSON !== "function") throw new Error("This Premiere build cannot check transcripts");
const present = await TranscriptSupport.probeTranscriptExport(function () {
return ppro.Transcript.exportToJSON(context.clip);
});
return {
projectGuid: context.projectGuid,
projectItemId: context.projectItemId,
projectItemName: context.projectItemName,
hasTranscript: present,
method: "export-probe"
};
}
async function inspectCaptions() {
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
const sequence = await project.getActiveSequence();
if (!sequence) throw new Error("No active sequence");
const count = await sequence.getCaptionTrackCount();
const tracks = [];
for (let i = 0; i < count; i += 1) {
const track = await sequence.getCaptionTrack(i);
const items = await track.getTrackItems(ppro.Constants.TrackItemType.CLIP, false);
tracks.push({ id: track.id, index: await track.getIndex(), name: track.name, muted: await track.isMuted(), itemCount: items ? items.length : 0 });
}
return { sequenceId: String(sequence.guid), sequenceName: sequence.name, trackCount: count, tracks };
}
async function stateSnapshot() {
const project = await ppro.Project.getActiveProject();
const sequence = project && await project.getActiveSequence();
const position = sequence && await sequence.getPlayerPosition();
return {
revision: stateRevision,
projectOpen: !!project,
project: project ? { id: project.guid || null, name: project.name || null, path: project.path || null } : null,
sequenceOpen: !!sequence,
sequence: sequence ? { id: sequence.guid || null, name: sequence.name || null } : null,
playheadSeconds: position ? position.seconds : null
};
}
async function exportFrame(args, operation) {
assertNotCancelled(operation);
publishOperation("progress", operation, { phase: "preflight", progress: 0.1 });
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
const sequence = await project.getActiveSequence();
if (!sequence) throw new Error("No active sequence");
if (!args.outputDirectory) throw new Error("outputDirectory is required");
const outputDirectory = await workspaceBroker.assertPathAllowed(args.outputDirectory, { label: "outputDirectory", kind: "directory" });
const filename = Protocol.safeFilename(args.filename);
const exporterFilename = Protocol.exporterFrameName(filename);
const position = args.seconds == null ? await sequence.getPlayerPosition() : await tickTime(args.seconds);
const size = await sequence.getFrameSize();
const width = positiveInt(args.width, size.width), height = positiveInt(args.height, size.height);
assertNotCancelled(operation);
operation.phase = "host_call";
publishOperation("progress", operation, { phase: "host_call", progress: 0.4, cancellable: false });
const returned = await ppro.Exporter.exportSequenceFrame(sequence, position, exporterFilename, outputDirectory, width, height);
if (returned !== true) throw new Error("Premiere did not confirm frame export; no output path is reported");
const path = Protocol.joinPath(outputDirectory, filename);
return {
path, width, height, seconds: position.seconds, exporterResult: returned,
operation: Protocol.operationSemantics({
mutatesProject: false,
verificationStatus: "not_verified",
verificationBoundary: "exporter_return_value",
verificationEvidence: [{ type: "host_return", value: returned }],
cancellationSupported: true
})
};
}
async function tickTime(seconds) {
if (ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function") return ppro.TickTime.createWithSeconds(Number(seconds));
throw new Error("This Premiere build cannot create TickTime; omit seconds to capture the playhead");
}
function positiveInt(value, fallback) { const n = Number(value == null ? fallback : value); if (!Number.isFinite(n) || n <= 0) throw new Error("frame dimensions must be positive"); return Math.round(n); }
async function dispatch(raw) {
let cmd;
let operation;
try {
cmd = Protocol.parseCommand(raw);
if (cmd.command === "operation.cancel") {
const result = operationTracker.requestCancel(cmd.args.requestId);
send(Protocol.envelope("result", { ok: true, result }, cmd.requestId));
return;
}
operation = operationTracker.begin(cmd.requestId, cmd.command);
publishOperation("started", operation, { phase: "preflight", progress: 0 });
let result;
if (cmd.command === "capabilities.get") result = await capabilities();
else if (cmd.command === "state.get") result = await stateSnapshot();
else if (cmd.command === "frame.export") result = await exportFrame(cmd.args, operation);
else if (cmd.command === "transcript.export") result = await exportTranscript(cmd.args);
else if (cmd.command === "transcript.search") result = await searchTranscript(cmd.args);
else if (cmd.command === "transcript.has") result = await hasTranscript(cmd.args);
else if (cmd.command === "captions.inspect") result = await inspectCaptions();
else {
assertNotCancelled(operation);
operation.phase = "host_call";
result = await commandRegistry.dispatch(cmd.command, cmd.args);
}
const response = Protocol.envelope("result", {
ok: true,
result,
operation: result && result.operation
? result.operation
: Protocol.operationSemantics(
cmd.command.indexOf("transition.video.") === 0 && cmd.command !== "transition.video.list"
? {
mutatesProject: true,
verificationStatus: "verified",
verificationBoundary: "project_executeTransaction_return",
verificationEvidence: [{ type: "transaction", accepted: true }],
undoSupported: true,
undoLabel: cmd.command === "transition.video.add"
? "Add video transition"
: "Remove video transition",
transactionActionGroup: true,
cancellationSupported: true
}
: {
mutatesProject: false,
verificationStatus: "verified",
verificationBoundary: "host_snapshot"
}
)
}, cmd.requestId);
// Validate the exact response before emitting a terminal success event. If
// serialization rejects an oversized result, the catch path emits only
// `failed`, never both `completed` and `failed` for one operation.
Protocol.serializeEnvelope(response);
publishOperation("completed", operation, { phase: "complete", progress: 1 });
send(response);
} catch (error) {
const cancelled = error && error.code === "UXP_OPERATION_CANCELLED";
const errorCode = cancelled
? "UXP_OPERATION_CANCELLED"
: error && typeof error.code === "string" && /^UXP_[A-Z0-9_]+$/.test(error.code)
? error.code
: "UXP_COMMAND_FAILED";
if (operation) publishOperation(cancelled ? "cancelled" : "failed", operation, {
phase: operation.phase, progress: null, error: error.message || String(error)
});
send(Protocol.envelope("result", {
ok: false,
error: {
code: errorCode,
message: error.message || String(error),
operation: Protocol.operationSemantics({ cancellationSupported: true })
}
}, cmd && cmd.requestId));
} finally {
if (operation) operationTracker.finish(operation);
}
}
function connect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
if (socket) try { socket.onclose = null; socket.close(); } catch (_) {}
const configuredUrl = document.getElementById("bridge-url").value;
const token = document.getElementById("bridge-token").value;
let url;
try {
url = WorkspaceSupport.validateLoopbackBridgeUrl(configuredUrl);
if (token) url.searchParams.set("token", token);
url = url.toString();
} catch (_) {
return scheduleReconnect("Invalid bridge URL");
}
setStatus("Connecting to " + url.origin + url.pathname);
try { socket = new WebSocket(url); } catch (e) { return scheduleReconnect(e.message); }
socket.onopen = async () => { setStatus("Connected"); send(Protocol.envelope("hello", await capabilities())); publishState("connected"); };
socket.onmessage = (event) => dispatch(event.data);
socket.onerror = () => setStatus("Bridge connection error");
socket.onclose = () => { void commandRegistry.dispose(); scheduleReconnect("Disconnected"); };
}
function scheduleReconnect(message) { setStatus(message + "; retrying in 2s"); reconnectTimer = setTimeout(connect, 2000); }
function send(value) { if (socket && socket.readyState === WebSocket.OPEN) socket.send(Protocol.serializeEnvelope(value)); }
function disconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = null;
if (socket) try { socket.onclose = null; socket.close(); } catch (_) {}
socket = null;
}
function publishOperation(name, operation, detail) {
send(Protocol.operationEvent(name, operation, detail));
}
function assertNotCancelled(operation) {
if (!operation || !operation.cancelRequested) return;
const error = new Error("Operation cancelled before the Premiere host call");
error.code = "UXP_OPERATION_CANCELLED";
throw error;
}
function supportedHostEvents() {
const constants = ppro.Constants || {};
const project = constants.ProjectEvent || {};
const sequence = constants.SequenceEvent || {};
const operation = constants.OperationCompleteEvent || {};
const encoder = ppro.EncoderManager || {};
const snapEvents = typeof EventSupport.createTimelineSnapEventDefinitions === "function"
? EventSupport.createTimelineSnapEventDefinitions(ppro.SnapEvent)
: [];
const operationBoundaryEvents = typeof EventSupport.createOperationBoundaryEventDefinitions === "function"
? EventSupport.createOperationBoundaryEventDefinitions(ppro.OperationCompleteEvent)
: [];
return [
hostEvent("project", "project.opened", project.OPENED, true),
hostEvent("project", "project.closed", project.CLOSED, true),
hostEvent("project", "project.dirty", project.DIRTY, true),
hostEvent("project", "project.activated", project.ACTIVATED, true),
hostEvent("project", "project.selection.changed", project.PROJECT_ITEM_SELECTION_CHANGED, true),
hostEvent("sequence", "sequence.activated", sequence.ACTIVATED, true),
hostEvent("sequence", "sequence.closed", sequence.CLOSED, true),
hostEvent("sequence", "sequence.selection.changed", sequence.SELECTION_CHANGED, true),
hostEvent("operation", "operation.import.complete", operation.IMPORT_MEDIA_COMPLETE || operation.EVENT_IMPORT_MEDIA_COMPLETE, false),
hostEvent("operation", "operation.export.complete", operation.EXPORT_MEDIA_COMPLETE || operation.EVENT_EXPORT_MEDIA_COMPLETE, false),
hostEvent("operation", "operation.effect.drop.complete", operation.EFFECT_DROP_COMPLETE || operation.EVENT_EFFECT_DROP_COMPLETE, false),
hostEvent("operation", "operation.generative.extend.complete", operation.GENERATIVE_EXTEND_COMPLETE || operation.EVENT_GENERATIVE_EXTEND_COMPLETE, false),
hostEvent("encoder", "encoder.queued", encoder.EVENT_RENDER_QUEUE, false),
hostEvent("encoder", "encoder.progress", encoder.EVENT_RENDER_PROGRESS, false, "encoder.progress"),
hostEvent("encoder", "encoder.complete", encoder.EVENT_RENDER_COMPLETE, false),
hostEvent("encoder", "encoder.error", encoder.EVENT_RENDER_ERROR, false),
hostEvent("encoder", "encoder.cancelled", encoder.EVENT_RENDER_CANCEL, false)
].concat(snapEvents, operationBoundaryEvents).filter((event) => event.eventName !== undefined && event.eventName !== null);
}
function hostEvent(category, name, eventName, stateInvalidating, coalesceKey) {
return { category, name, eventName, stateInvalidating, coalesceKey: coalesceKey || null };
}
function subscribeHostEvents() {
if (!ppro.EventManager) return;
if (!eventSubscriptions.length) {
supportedHostEvents().forEach((definition) => {
const handler = (event) => recordHostEvent(definition, event);
ppro.EventManager.addGlobalEventListener(definition.eventName, handler, false);
eventSubscriptions.push({ eventName: definition.eventName, handler });
});
}
void rebindTrackEvents();
}
function unsubscribeHostEvents() {
if (!ppro.EventManager) return;
unsubscribeTrackEvents();
eventSubscriptions.splice(0).forEach(({ eventName, handler }) => {
try { ppro.EventManager.removeGlobalEventListener(eventName, handler, false); } catch (_) {}
});
}
function recordHostEvent(definition, event) {
const detail = {};
if (event && typeof event === "object") {
if (Number.isFinite(Number(event.state))) detail.state = Number(event.state);
if (Number.isFinite(Number(event.progress))) detail.progress = Number(event.progress);
}
const receipt = eventJournal.recordHostEvent({
category: definition.category,
name: definition.name,
detail,
coalesceKey: definition.coalesceKey
});
if (receipt) send(Protocol.envelope("event", { name: "premiere.host.event", receipt }));
if (definition.stateInvalidating) publishState("host-event", definition.name);
if (definition.name === "project.opened" || definition.name === "project.closed" ||
definition.name === "project.activated" || definition.name === "sequence.activated" ||
definition.name === "sequence.closed") void rebindTrackEvents();
}
async function rebindTrackEvents() {
if (!ppro.EventManager || typeof ppro.EventManager.addEventListener !== "function") return;
const generation = ++targetSubscriptionGeneration;
unsubscribeTrackEvents(false);
try {
const project = await ppro.Project.getActiveProject();
const sequence = project && await project.getActiveSequence();
if (!sequence || generation !== targetSubscriptionGeneration) return;
const definitions = [
{ mediaType: "video", countMethod: "getVideoTrackCount", getMethod: "getVideoTrack", api: ppro.VideoTrack },
{ mediaType: "audio", countMethod: "getAudioTrackCount", getMethod: "getAudioTrack", api: ppro.AudioTrack }
];
for (let d = 0; d < definitions.length; d += 1) {
const definition = definitions[d];
if (typeof sequence[definition.countMethod] !== "function" || typeof sequence[definition.getMethod] !== "function") continue;
const count = Math.min(256, Math.max(0, Number(await sequence[definition.countMethod]()) || 0));
for (let index = 0; index < count; index += 1) {
if (generation !== targetSubscriptionGeneration) return;
const track = await sequence[definition.getMethod](index);
if (generation !== targetSubscriptionGeneration) return;
if (!track) continue;
const eventKinds = [
["changed", definition.api && definition.api.EVENT_TRACK_CHANGED],
["info_changed", definition.api && definition.api.EVENT_TRACK_INFO_CHANGED],
["lock_changed", definition.api && definition.api.EVENT_TRACK_LOCK_CHANGED]
];
for (let e = 0; e < eventKinds.length; e += 1) {
const kind = eventKinds[e][0], eventName = eventKinds[e][1];
if (eventName == null) continue;
const handler = () => recordTrackEvent(definition.mediaType, index, kind);
ppro.EventManager.addEventListener(track, eventName, handler, false);
targetEventSubscriptions.push({ target: track, eventName, handler });
}
}
}
} catch (_) {}
}
function unsubscribeTrackEvents(incrementGeneration = true) {
if (incrementGeneration) targetSubscriptionGeneration += 1;
targetEventSubscriptions.splice(0).forEach(({ target, eventName, handler }) => {
try { ppro.EventManager.removeEventListener(target, eventName, handler); } catch (_) {}
});
}
function recordTrackEvent(mediaType, trackIndex, eventKind) {
const receipt = eventJournal.recordHostEvent({
category: "track",
name: "track." + mediaType + "." + eventKind,
detail: { mediaType, trackIndex, eventKind }
});
if (receipt) send(Protocol.envelope("event", { name: "premiere.host.event", receipt }));
}
function startFallbackPolling() {
if (!fallbackPollTimer) fallbackPollTimer = setInterval(() => publishState("fallback-poll"), 5000);
}
function stopFallbackPolling() {
if (fallbackPollTimer) clearInterval(fallbackPollTimer);
fallbackPollTimer = null;
}
async function publishState(reason, hostEvent) {
try {
stateRevision += 1;
const state = await stateSnapshot();
const comparable = Object.assign({}, state, { revision: 0 });
const encoded = JSON.stringify(comparable);
if (reason !== "fallback-poll" || encoded !== lastState) {
lastState = encoded;
send(Protocol.envelope("event", {
name: "premiere.state.changed",
reason,
hostEvent: hostEvent || null,
state
}));
}
} catch (e) { setStatus(e.message); }
}
function setStatus(value) { const el = document.getElementById("status"); if (el) el.textContent = value; }
async function chooseWorkspace() {
try {
setStatus("Choose the folder that contains approved media, presets, and exports");
await workspaceBroker.requestRoot();
renderWorkspaceStatus();
setStatus("Workspace access granted");
} catch (error) {
setStatus(error.message || String(error));
}
}
async function revokeWorkspace() {
try {
await workspaceBroker.revoke();
renderWorkspaceStatus();
setStatus("Workspace access revoked");
} catch (error) {
setStatus(error.message || String(error));
}
}
function renderWorkspaceStatus() {
const element = document.getElementById("workspace-status"), value = workspaceBroker.status();
if (element) element.textContent = value.configured
? "Approved folder: " + value.rootName + "\nPersistent access: yes\nNative path: redacted\nCanonical path validation: " + value.canonicalPathValidation
: "No approved workspace folder";
}
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; connect-src ws://127.0.0.1:* ws://localhost:*; object-src 'none'; base-uri 'none'">
<link rel="stylesheet" href="styles.css"></head><body>
<h3>MCP for Adobe Premiere Pro</h3>
<label for="bridge-url">Bridge URL</label><input id="bridge-url" value="ws://127.0.0.1:7777/uxp" spellcheck="false" autocomplete="off">
<label for="bridge-token">Bridge token</label><input id="bridge-token" type="password" autocomplete="off">
<button id="connect" aria-controls="status">Connect</button><button id="refresh" aria-controls="status">Refresh state</button>
<h4>Approved workspace</h4>
<p class="help">File-based UXP workflows are limited to one folder you choose. The bridge never returns its native path.</p>
<button id="choose-workspace" aria-controls="workspace-status">Choose folder</button><button id="revoke-workspace" aria-controls="workspace-status">Revoke access</button>
<pre id="workspace-status" role="status" aria-live="polite" aria-atomic="true">Loading workspace access…</pre>
<pre id="status" role="status" aria-live="polite" aria-atomic="true" tabindex="0">Starting…</pre>
<script src="protocol.cjs"></script><script src="transcript.cjs"></script><script src="transcript-import.cjs"></script><script src="workspace.cjs"></script><script src="events.cjs"></script><script src="hybrid-benchmark.cjs"></script><script src="workflows.cjs"></script><script src="project-item-color-label-locks.cjs"></script><script src="advanced-workflows.cjs"></script><script src="dialogue-workflows.cjs"></script><script src="timeline-source-label-workflows.cjs"></script><script src="sequence-preview-frame-workflows.cjs"></script><script src="source-media-provenance-workflows.cjs"></script><script src="source-proxy-workflows.cjs"></script><script src="tick-time-arithmetic-workflows.cjs"></script><script src="object-mask-audit-workflows.cjs"></script><script src="unique-identity-workflows.cjs"></script><script src="effect-parameter-catalog-workflows.cjs"></script><script src="track-item-mutation-locks.cjs"></script><script src="slip-workflows.cjs"></script><script src="slide-workflows.cjs"></script><script src="clone-workflows.cjs"></script><script src="ripple-delete-workflows.cjs"></script><script src="next-workflows.cjs"></script><script src="commands.cjs"></script><script src="index.cjs"></script>
</body></html>
+22
View File
@@ -0,0 +1,22 @@
{
"manifestVersion": 5,
"id": "com.ppmcp.premiere.uxp",
"name": "MCP for Adobe Premiere Pro",
"version": "1.14.9",
"main": "index.html",
"host": { "app": "premierepro", "minVersion": "25.6.0" },
"entrypoints": [
{
"type": "panel",
"id": "mcpBridgePanel",
"label": { "default": "MCP for Adobe Premiere Pro" },
"minimumSize": { "width": 320, "height": 240 },
"preferredDockedSize": { "width": 380, "height": 520 },
"preferredFloatingSize": { "width": 420, "height": 560 }
}
],
"requiredPermissions": {
"localFileSystem": "request",
"network": { "domains": "all" }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,173 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpObjectMaskAuditWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// ObjectMaskUtils intentionally exposes only a yes/no answer. This audit
// makes that answer useful for bounded review without inventing a mask
// count, location, quality, editability, tracking state, or render result.
function createObjectMaskAuditWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const MAX_SEQUENCES = 64;
const definitions = {
"objectMask.audit": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "26.3.0",
probe: canAuditObjectMasks,
handler: auditObjectMasks
}
};
function canAuditObjectMasks() {
return !!(ppro && ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.ObjectMaskUtils && typeof ppro.ObjectMaskUtils.hasObjectMask === "function");
}
async function auditObjectMasks(args) {
assertOnlyKeys(args, ["expectedProjectGuid", "sequenceIds"]);
const requestedIds = requestedSequenceIds(args.sequenceIds);
const project = await activeProject();
const projectGuid = requiredGuid(project.guid, "active project GUID");
if (args.expectedProjectGuid != null && requiredGuid(args.expectedProjectGuid, "expectedProjectGuid") !== projectGuid) {
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "The active project differs from expectedProjectGuid; inspect the current object-mask state again");
}
const projectHasObjectMask = objectMaskValue(project);
const first = await targetSequences(project, requestedIds);
const firstResults = first.map(function (entry) {
return { id: entry.id, name: entry.name, hasObjectMask: objectMaskValue(entry.sequence) };
});
const activeAfter = await activeProject();
if (requiredGuid(activeAfter.guid, "active project GUID after audit") !== projectGuid) {
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "The active project changed while object masks were being audited");
}
const second = await targetSequences(activeAfter, requestedIds);
const secondResults = second.map(function (entry) {
return { id: entry.id, name: entry.name, hasObjectMask: objectMaskValue(entry.sequence) };
});
const projectHasObjectMaskAfter = objectMaskValue(activeAfter);
if (projectHasObjectMaskAfter !== projectHasObjectMask || !sameAudit(firstResults, secondResults)) {
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "The project, sequence identities, or object-mask results changed during the audit; retry");
}
const maskedSequenceCount = firstResults.filter(function (entry) { return entry.hasObjectMask; }).length;
return {
projectGuid,
scope: requestedIds ? "explicit_sequences" : "all_project_sequences",
projectHasObjectMask,
sequenceCount: firstResults.length,
maskedSequenceCount,
sequences: firstResults,
verificationBoundary: "bounded_project_and_sequence_object_mask_double_readback"
};
}
async function activeProject() {
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
return project;
}
function requestedSequenceIds(value) {
if (value == null) return null;
if (!Array.isArray(value) || !value.length || value.length > MAX_SEQUENCES) {
throw commandError("UXP_INVALID_ARGUMENT", "sequenceIds must contain 1-" + MAX_SEQUENCES + " exact sequence GUIDs");
}
const ids = value.map(function (entry, index) {
return requiredGuid(entry, "sequenceIds[" + index + "]");
});
if (new Set(ids).size !== ids.length) throw commandError("UXP_INVALID_ARGUMENT", "sequenceIds must not contain duplicates");
return ids.sort();
}
async function targetSequences(project, requestedIds) {
const values = requestedIds ? await sequencesById(project, requestedIds) : await allSequences(project);
const seen = new Set(), snapshots = [];
for (let index = 0; index < values.length; index += 1) {
const sequence = values[index];
const id = requiredGuid(sequence && sequence.guid, "sequence GUID");
if (seen.has(id)) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned duplicate sequence GUIDs during object-mask audit");
seen.add(id);
snapshots.push({ sequence, id, name: boundedName(sequence && sequence.name) });
}
snapshots.sort(function (left, right) { return left.id.localeCompare(right.id); });
return snapshots;
}
async function allSequences(project) {
if (typeof project.getSequences !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented project sequence enumeration");
}
const values = Array.from(await project.getSequences() || []);
if (values.length > MAX_SEQUENCES) {
throw commandError("UXP_PROJECT_TOO_LARGE", "Object-mask audit is capped at " + MAX_SEQUENCES + " sequences; pass at most " + MAX_SEQUENCES + " exact sequenceIds instead");
}
return values;
}
async function sequencesById(project, ids) {
if (!ppro.Guid || typeof ppro.Guid.fromString !== "function" || typeof project.getSequence !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented exact sequence GUID lookup");
}
const values = [];
for (let index = 0; index < ids.length; index += 1) {
let sequence;
try { sequence = await project.getSequence(ppro.Guid.fromString(ids[index])); } catch (_) { sequence = null; }
if (!sequence || requiredGuid(sequence.guid, "resolved sequence GUID") !== ids[index]) {
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "A requested sequence no longer resolves to its exact GUID; retry the audit");
}
values.push(sequence);
}
return values;
}
function objectMaskValue(target) {
let value;
try { value = ppro.ObjectMaskUtils.hasObjectMask(target); } catch (error) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere could not report object-mask presence: " + (error && error.message || String(error)));
}
if (typeof value !== "boolean") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned a non-boolean object-mask result");
return value;
}
function sameAudit(left, right) {
return left.length === right.length && left.every(function (entry, index) {
const other = right[index];
return other && entry.id === other.id && entry.name === other.name && entry.hasObjectMask === other.hasObjectMask;
});
}
return definitions;
}
function assertOnlyKeys(value, allowed) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "arguments must be an object");
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
}
function requiredGuid(value, name) {
let text = "";
try { text = String(value && typeof value.toString === "function" ? value.toString() : value || ""); } catch (_) {}
if (!text || text.length > 512) throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded non-empty GUID");
return text;
}
function boundedName(value) {
const name = String(value == null ? "" : value);
if (name.length > 255) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an overlong sequence name");
return name;
}
function commandError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { createObjectMaskAuditWorkflowDefinitions };
});
@@ -0,0 +1,31 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpProjectItemColorLabelLocks = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// A source item's color label is project-global: the same item can appear on
// multiple timeline tracks or sequences. Keep every bridge color-label
// action for that item behind one tail so a second operation cannot build an
// action from a label snapshot that the first operation has already changed.
function createProjectItemColorLabelLocks() {
const tails = new Map();
function withProjectItemColorLabelLock(key, operation) {
const previous = tails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
tails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (tails.get(key) === tail) tails.delete(key);
});
}
return { withProjectItemColorLabelLock };
}
return { createProjectItemColorLabelLocks };
});
+153
View File
@@ -0,0 +1,153 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpProtocol = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const PROTOCOL_VERSION = 2;
const MAX_COMMAND_BYTES = 64 * 1024;
const MAX_RESULT_BYTES = 1024 * 1024;
const COMMAND_NAME = /^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)+$/;
function envelope(type, payload, requestId) {
const value = { protocolVersion: PROTOCOL_VERSION, type, payload: payload || {}, sentAt: new Date().toISOString() };
if (requestId) value.requestId = requestId;
return value;
}
function utf8ByteLength(value) {
const text = String(value);
let bytes = 0;
for (let index = 0; index < text.length; index += 1) {
const code = text.charCodeAt(index);
if (code < 0x80) bytes += 1;
else if (code < 0x800) bytes += 2;
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length && text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
bytes += 4;
index += 1;
} else bytes += 3;
}
return bytes;
}
function resultTooLarge() {
const error = new Error("UXP bridge result exceeds 1 MiB after UTF-8 serialization");
error.code = "UXP_RESULT_TOO_LARGE";
return error;
}
function serializeEnvelope(value) {
const encoded = JSON.stringify(value);
if (utf8ByteLength(encoded) > MAX_RESULT_BYTES) throw resultTooLarge();
return encoded;
}
function assertResultSize(result) {
// Use the longest legal request id and the same fallback operation payload
// dispatch adds to read-only command results, so this is conservative for
// metadata snapshots before the exact final envelope is serialized.
serializeEnvelope(envelope("result", {
ok: true,
result,
operation: operationSemantics({
mutatesProject: false,
verificationStatus: "verified",
verificationBoundary: "host_snapshot"
})
}, "x".repeat(128)));
return result;
}
function parseCommand(raw) {
if (typeof raw === "string" && utf8ByteLength(raw) > MAX_COMMAND_BYTES) throw new Error("UXP bridge command exceeds 64 KiB");
const value = typeof raw === "string" ? JSON.parse(raw) : raw;
if (!isPlainObject(value) || value.type !== "command" || typeof value.command !== "string" || !COMMAND_NAME.test(value.command)) throw new Error("Invalid UXP bridge command");
if (value.protocolVersion != null && value.protocolVersion !== PROTOCOL_VERSION) throw new Error("Unsupported UXP protocol version: " + value.protocolVersion);
if (value.requestId != null && (typeof value.requestId !== "string" || value.requestId.length < 1 || value.requestId.length > 128)) throw new Error("requestId must be a non-empty string of at most 128 characters");
if (value.args != null && !isPlainObject(value.args)) throw new Error("command args must be an object");
return { requestId: value.requestId || null, command: value.command, args: value.args || {} };
}
function isPlainObject(value) { return !!value && typeof value === "object" && !Array.isArray(value); }
function operationEvent(name, operation, detail) {
return envelope("event", {
name: "premiere.operation." + name,
operation: Object.assign({
requestId: operation.requestId,
command: operation.command
}, detail || {})
}, operation.requestId);
}
function operationSemantics(options) {
const value = options || {};
return {
mutatesProject: value.mutatesProject === true,
verification: {
status: value.verificationStatus || "not_verified",
boundary: value.verificationBoundary || "command_return_value",
evidence: value.verificationEvidence || []
},
undo: {
supported: value.undoSupported === true,
boundary: value.undoSupported === true ? "premiere_undo_history" : "not_undoable",
label: value.undoLabel || null
},
transaction: {
actionGroup: value.transactionActionGroup === true,
boundary: value.transactionActionGroup === true ? "project_executeTransaction" : "single_host_call",
atomicRollback: false
},
cancellation: {
supported: value.cancellationSupported === true,
boundary: value.cancellationBoundary || "before_non_cancellable_host_call"
}
};
}
function createOperationTracker() {
const operations = new Map();
return {
begin(requestId, command) {
const operation = {
requestId: requestId || null,
command,
phase: "preflight",
cancelRequested: false,
complete: false
};
if (requestId) operations.set(requestId, operation);
return operation;
},
get(requestId) { return requestId ? operations.get(requestId) || null : null; },
requestCancel(requestId) {
const operation = this.get(requestId);
if (!operation || operation.complete) return { accepted: false, reason: "operation_not_active" };
if (operation.phase !== "preflight") return { accepted: false, reason: "host_call_not_cancellable" };
operation.cancelRequested = true;
return { accepted: true, reason: "cancellation_requested" };
},
finish(operation) {
operation.complete = true;
if (operation.requestId) operations.delete(operation.requestId);
}
};
}
function safeFilename(value) {
const name = String(value || "mcp-frame.png");
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.png$/i.test(name)) throw new Error("filename must be a simple .png name");
return name;
}
// Premiere's frame exporter selects PNG from the name and appends that extension
// itself. Keep the public, fully qualified filename for reporting, but pass the
// bare stem to the host so `frame.png` does not become `frame.png.png`.
function exporterFrameName(value) { return safeFilename(value).replace(/\.png$/i, ""); }
function joinPath(dir, name) { return /[\\\/]$/.test(dir) ? dir + name : dir + "/" + name; }
return {
PROTOCOL_VERSION,
MAX_COMMAND_BYTES,
MAX_RESULT_BYTES,
envelope,
utf8ByteLength,
serializeEnvelope,
assertResultSize,
parseCommand,
operationEvent,
operationSemantics,
createOperationTracker,
safeFilename,
exporterFrameName,
joinPath
};
});
@@ -0,0 +1,360 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpRippleDeleteWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// A coordinate-bound ripple delete is deliberately narrower than the legacy
// QE command: it removes exactly one item only when its immediate successor
// is contiguous, so post-readback can prove the closed cut without reading
// unrelated items on the track.
function createRippleDeleteWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const fallbackTails = new Map();
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
? deps.locks
: { withTrackMutationLock: localLock };
const definitions = {
"trackItem.rippleDelete.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseRippleDelete,
handler: inspectRippleDelete
},
"trackItem.rippleDelete": {
destructive: true,
undoable: true,
idempotent: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseRippleDelete,
handler: rippleDelete
}
};
function canUseRippleDelete() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.SequenceEditor && typeof ppro.SequenceEditor.getEditor === "function" &&
ppro.TrackItemSelection && typeof ppro.TrackItemSelection.createEmptySelection === "function" &&
ppro.Constants && ppro.Constants.MediaType);
}
async function inspectRippleDelete(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
const context = await activeTarget(args, false);
const snapshot = await rippleSnapshot(context);
assertRippleSupported(snapshot);
return snapshot;
}
async function rippleDelete(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "confirmRippleDelete", "operationId"]);
requireConfirmation(args.confirmRippleDelete);
requireOperationId(args.operationId);
const target = targetCoordinates(args), expected = requiredSnapshot(args.expectedSnapshot);
assertExpectedTarget(expected, target);
const initial = await activeTarget(target, true);
if (initial.projectGuid !== expected.projectGuid || initial.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed ripple-delete snapshot");
}
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
return locks.withTrackMutationLock(key, async function () {
// This full preflight occurs after entering the same per-track tail as
// slips, slides, and append duplicates, so a distinct operation ID
// cannot construct a remove action from an obsolete timeline state.
let context;
try {
context = await activeTarget(target, true);
} catch (error) {
if (error && error.code === "UXP_TARGET_NOT_FOUND") {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed target no longer exists before the ripple-delete transaction");
}
throw error;
}
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the ripple-delete transaction");
}
let before;
try {
before = await rippleSnapshot(context);
} catch (error) {
if (error && error.code === "UXP_TARGET_UNSUPPORTED") {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed contiguous successor changed before the ripple-delete transaction");
}
throw error;
}
if (!sameSnapshot(before, expected)) {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed target or contiguous successor changed since inspection");
}
assertRippleSupported(before);
let committed = false;
context.project.lockedAccess(function () {
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before ripple-delete action creation");
}
const editor = ppro.SequenceEditor.getEditor(context.sequence);
const mediaType = ppro.Constants.MediaType[context.mediaType.toUpperCase()];
if (!editor || typeof editor.createRemoveItemsAction !== "function" || mediaType == null) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented ripple-delete actions");
}
const selection = createSingleItemSelection(context.item);
const action = editor.createRemoveItemsAction(selection, true, mediaType, false);
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the ripple-delete action");
committed = context.project.executeTransaction(function (compoundAction) {
if (compoundAction.addAction(action) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the ripple-delete action");
}
}, "Ripple delete timeline item");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the ripple-delete transaction");
// The successor now occupies the removed target's coordinate. No
// unrelated item field is read after the host transaction commits.
const afterContext = await activeTarget(target, true);
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed ripple delete");
}
const after = await rippleReadback(afterContext, before);
if (!sameRippleResult(before, after)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested contiguous ripple-delete result");
}
return {
rippleDeleted: true,
before,
after,
outcome: "verified",
verificationBoundary: "contiguous_successor_track_item_readback",
undoLabel: "Ripple delete timeline item"
};
});
}
async function activeTarget(args, requireTransaction) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
const target = targetCoordinates(args), resolved = await trackItemsAt(sequence, target);
const item = resolved.items[target.clipIndex];
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
return {
project,
sequence,
projectGuid: requiredGuid(project.guid, "active project GUID"),
sequenceId: requiredGuid(sequence.guid, "active sequence GUID"),
...target,
item,
items: resolved.items
};
}
async function trackItemsAt(sequence, target) {
const title = target.mediaType === "video" ? "Video" : "Audio";
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
}
const count = Number(await sequence[countMethod]());
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (!items.length) throw commandError("UXP_TARGET_NOT_FOUND", "The target track has no clip items");
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
return { items };
}
async function rippleSnapshot(context) {
const following = context.items[context.clipIndex + 1];
if (!following) throw commandError("UXP_TARGET_UNSUPPORTED", "Ripple delete requires one immediate following clip item on the same track");
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
clipIndex: context.clipIndex,
trackItemCount: context.items.length,
target: await itemSnapshot(context.item, "target"),
following: await itemSnapshot(following, "following")
};
}
async function rippleReadback(context, before) {
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
successorClipIndex: before.clipIndex,
trackItemCount: context.items.length,
successor: await itemSnapshot(context.item, "successor readback")
};
}
async function itemSnapshot(item, label) {
const sourceItem = await requiredMethod(item, "getProjectItem")();
const snapshot = {
projectItemId: requiredIdentifier(await requiredMethod(sourceItem, "getId")(), label + " source project-item ID"),
startSeconds: tickSeconds(await requiredMethod(item, "getStartTime")()),
endSeconds: tickSeconds(await requiredMethod(item, "getEndTime")()),
inSeconds: tickSeconds(await requiredMethod(item, "getInPoint")()),
outSeconds: tickSeconds(await requiredMethod(item, "getOutPoint")()),
durationSeconds: tickSeconds(await requiredMethod(item, "getDuration")()),
speed: Number(await requiredMethod(item, "getSpeed")()),
reversed: Boolean(await requiredMethod(item, "isSpeedReversed")())
};
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " " + key);
}
}
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " speed");
}
return snapshot;
}
function assertRippleSupported(snapshot) {
if (snapshot.clipIndex >= snapshot.trackItemCount - 1) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Ripple delete requires a following clip item to close the requested cut");
}
const target = snapshot.target, following = snapshot.following;
if (target.endSeconds <= target.startSeconds || following.endSeconds <= following.startSeconds ||
target.durationSeconds <= 0 || following.durationSeconds <= 0 ||
!numbersEqual(target.endSeconds, following.startSeconds)) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Ripple delete supports only positive-duration items with one contiguous same-track successor");
}
}
function sameRippleResult(before, after) {
const targetTimelineDuration = before.target.endSeconds - before.target.startSeconds;
const following = before.following, successor = after.successor;
return after.projectGuid === before.projectGuid && after.sequenceId === before.sequenceId &&
after.mediaType === before.mediaType && after.trackIndex === before.trackIndex &&
after.successorClipIndex === before.clipIndex && after.trackItemCount === before.trackItemCount - 1 &&
successor.projectItemId === following.projectItemId &&
numbersEqual(successor.startSeconds, following.startSeconds - targetTimelineDuration) &&
numbersEqual(successor.endSeconds, following.endSeconds - targetTimelineDuration) &&
numbersEqual(successor.inSeconds, following.inSeconds) && numbersEqual(successor.outSeconds, following.outSeconds) &&
numbersEqual(successor.durationSeconds, following.durationSeconds) &&
numbersEqual(successor.speed, following.speed) && successor.reversed === following.reversed;
}
function sameSnapshot(left, right) {
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
left.mediaType === right.mediaType && left.trackIndex === right.trackIndex &&
left.clipIndex === right.clipIndex && left.trackItemCount === right.trackItemCount &&
sameItem(left.target, right.target) && sameItem(left.following, right.following);
}
function sameItem(left, right) {
return left.projectItemId === right.projectItemId && numbersEqual(left.startSeconds, right.startSeconds) &&
numbersEqual(left.endSeconds, right.endSeconds) && numbersEqual(left.inSeconds, right.inSeconds) &&
numbersEqual(left.outSeconds, right.outSeconds) && numbersEqual(left.durationSeconds, right.durationSeconds) &&
numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
}
function createSingleItemSelection(item) {
let selection = null;
const created = ppro.TrackItemSelection.createEmptySelection(function (value) { selection = value; });
if (created !== true || !selection || typeof selection.addItem !== "function" || selection.addItem(item, false) !== true) {
throw commandError("UXP_SELECTION_REJECTED", "Premiere rejected the bounded ripple-delete selection");
}
return selection;
}
function targetCoordinates(args) {
return {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
};
}
function requiredSnapshot(value) {
assertObject(value, "expectedSnapshot");
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "trackItemCount", "target", "following"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
return {
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
trackItemCount: positiveInt(value.trackItemCount, "expectedSnapshot.trackItemCount"),
target: requiredItemSnapshot(value.target, "expectedSnapshot.target"),
following: requiredItemSnapshot(value.following, "expectedSnapshot.following")
};
}
function requiredItemSnapshot(value, name) {
assertObject(value, name);
const allowed = ["projectItemId", "startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
return {
projectItemId: requiredIdentifier(value.projectItemId, name + ".projectItemId"),
startSeconds: boundedSeconds(value.startSeconds, name + ".startSeconds"),
endSeconds: boundedSeconds(value.endSeconds, name + ".endSeconds"),
inSeconds: boundedSeconds(value.inSeconds, name + ".inSeconds"),
outSeconds: boundedSeconds(value.outSeconds, name + ".outSeconds"),
durationSeconds: boundedSeconds(value.durationSeconds, name + ".durationSeconds"),
speed: boundedSpeed(value.speed, name + ".speed"),
reversed: requiredBoolean(value.reversed, name + ".reversed")
};
}
function assertExpectedTarget(expected, target) {
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
}
}
function localLock(key, operation) {
const previous = fallbackTails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
fallbackTails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
});
}
function requiredMethod(target, name) { if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The required item does not expose " + name); return target[name].bind(target); }
function assertObject(value, name) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object"); }
function assertOnlyKeys(value, allowed) { assertObject(value, "arguments"); for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }
function enumValue(value, name, allowed) { if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
function nonNegativeInt(value, name) { if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511"); return value; }
function positiveInt(value, name) { if (!Number.isSafeInteger(value) || value < 1 || value > 512) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 1 to 512"); return value; }
function boundedSeconds(value, name) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400"); return seconds; }
function boundedSpeed(value, name) { const speed = Number(value); if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100"); return speed; }
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
function requireConfirmation(value) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "trackItem.rippleDelete requires confirmRippleDelete: true"); }
function requireOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); }
function requiredGuid(value, name) { const guid = guidString(value); if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return guid; }
function requiredIdentifier(value, name) { const id = guidString(value); if (!id || id.length > 512 || id.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return id; }
function guidString(value) { if (value == null) return ""; try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; } }
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
function numbersEqual(left, right) { return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001; }
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return definitions;
}
return { createRippleDeleteWorkflowDefinitions };
});
@@ -0,0 +1,261 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpSequencePreviewFrameWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// This workflow intentionally owns its per-sequence lock instead of extending
// the broad sequence-settings profile. A reviewed preview rectangle must not
// be silently applied after another invocation has changed the same sequence.
function createSequencePreviewFrameWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const tails = new Map();
const definitions = {
"sequence.previewFrame.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "26.3.0",
probe: canUseSequencePreviewFrame,
handler: inspectSequencePreviewFrame
},
"sequence.previewFrame.update": {
destructive: true,
undoable: true,
idempotent: true,
targetCapabilityProbe: true,
minHostVersion: "26.3.0",
probe: canUseSequencePreviewFrame,
handler: updateSequencePreviewFrame
}
};
function canUseSequencePreviewFrame() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" && typeof ppro.RectF === "function");
}
async function inspectSequencePreviewFrame(args) {
assertOnlyKeys(args, ["sequenceId"]);
const sequenceId = requiredGuid(args.sequenceId, "sequenceId");
const first = await targetSequence(sequenceId, false);
const before = await previewFrameSnapshot(first);
const second = await targetSequence(sequenceId, false);
const after = await previewFrameSnapshot(second);
if (!sameSnapshot(before, after)) {
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The reviewed project, sequence, or preview frame changed while it was being inspected");
}
return after;
}
async function updateSequencePreviewFrame(args) {
assertOnlyKeys(args, ["sequenceId", "previewWidth", "previewHeight", "expectedSnapshot", "confirmSetPreviewFrame", "operationId"]);
const sequenceId = requiredGuid(args.sequenceId, "sequenceId");
const requested = previewRect(args.previewWidth, args.previewHeight, "requested preview frame");
const expected = requiredSnapshot(args.expectedSnapshot);
requireConfirmation(args.confirmSetPreviewFrame);
requireOperationId(args.operationId);
if (expected.sequenceId !== sequenceId) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot.sequenceId must exactly match sequenceId");
}
// The complete reviewed owner identity is enough to join the tail before
// *any* live preflight getter. If the project has changed, the guarded
// snapshot inside the tail fails closed instead of selecting a new owner.
const lockKey = expected.projectGuid + "\u0000" + expected.sequenceId;
return withSequenceLock(lockKey, async function () {
// Re-resolve only after joining the owner-specific tail. A distinct
// operation ID cannot bypass the reviewed snapshot between preflight
// and action construction inside this bridge process.
let context;
try {
context = await targetSequence(sequenceId, true);
} catch (error) {
if (error && error.code === "UXP_TARGET_NOT_FOUND") {
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The reviewed sequence no longer resolves before preview-frame action creation");
}
throw error;
}
const before = await previewFrameSnapshot(context);
if (!sameSnapshot(before, expected)) {
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The reviewed preview frame changed before action creation");
}
const settings = await requiredMethod(context.sequence, "getSettings")();
if (!settings || typeof settings.setPreviewFrameRect !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose SequenceSettings.setPreviewFrameRect for this sequence");
}
const nativeRect = nativeRectFor(requested);
let accepted;
try {
accepted = await settings.setPreviewFrameRect(nativeRect);
} catch (error) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the requested preview frame rectangle: " + messageOf(error));
}
if (accepted !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere did not accept the requested preview frame rectangle");
let committed = false;
context.project.lockedAccess(function () {
if (requiredGuid(context.project.guid, "active project GUID") !== before.projectGuid ||
requiredGuid(context.sequence.guid, "target sequence GUID") !== before.sequenceId) {
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The active project or reviewed sequence changed before preview-frame action creation");
}
if (typeof context.sequence.createSetSettingsAction !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose Sequence.createSetSettingsAction for this sequence");
}
const action = context.sequence.createSetSettingsAction(settings);
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create a preview-frame settings action");
committed = context.project.executeTransaction(function (compoundAction) {
if (!compoundAction || typeof compoundAction.addAction !== "function" || compoundAction.addAction(action) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the preview-frame settings action");
}
}, "Set sequence preview frame");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the preview-frame settings transaction");
const after = await previewFrameSnapshot(await targetSequence(sequenceId, true));
if (after.projectGuid !== before.projectGuid || after.sequenceId !== before.sequenceId ||
after.previewWidth !== requested.width || after.previewHeight !== requested.height) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested preview frame after the transaction");
}
return {
previewFrameUpdated: true,
before,
after,
outcome: "verified",
verificationBoundary: "sequence_preview_frame_readback",
undoLabel: "Set sequence preview frame"
};
});
}
async function targetSequence(sequenceId, requireTransaction) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
const projectGuid = requiredGuid(project.guid, "active project GUID");
const sequences = await requiredMethod(project, "getSequences")();
if (!Array.isArray(sequences)) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid sequence collection");
if (sequences.length > 1024) throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 1024 sequences");
const sequence = sequences.find(function (candidate) {
return candidate && guidString(candidate.guid) === sequenceId;
});
if (!sequence) throw commandError("UXP_TARGET_NOT_FOUND", "sequenceId does not identify a sequence in the active project");
return { project, projectGuid, sequence, sequenceId: requiredGuid(sequence.guid, "target sequence GUID") };
}
async function previewFrameSnapshot(context) {
const settings = await requiredMethod(context.sequence, "getSettings")();
if (!settings || typeof settings.getPreviewFrameRect !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose SequenceSettings.getPreviewFrameRect for this sequence");
}
const rect = await settings.getPreviewFrameRect();
const dimensions = previewRect(rect && rect.width, rect && rect.height, "native preview frame");
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
previewWidth: dimensions.width,
previewHeight: dimensions.height
};
}
function nativeRectFor(requested) {
let rect;
try { rect = new ppro.RectF(); } catch (error) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere could not construct a documented RectF: " + messageOf(error));
}
if (!rect || typeof rect !== "object") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not construct a documented RectF");
try {
rect.width = requested.width;
rect.height = requested.height;
} catch (error) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere rejected the requested RectF dimensions: " + messageOf(error));
}
if (Number(rect.width) !== requested.width || Number(rect.height) !== requested.height) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not retain the requested RectF dimensions");
}
return rect;
}
function withSequenceLock(key, operation) {
const previous = tails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
tails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (tails.get(key) === tail) tails.delete(key);
});
}
return definitions;
}
function assertObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
}
function assertOnlyKeys(value, allowed) {
assertObject(value, "args");
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
}
function requiredMethod(value, method) {
if (!value || typeof value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
return value[method].bind(value);
}
function requiredSnapshot(value) {
assertObject(value, "expectedSnapshot");
const keys = ["projectGuid", "sequenceId", "previewWidth", "previewHeight"];
assertOnlyKeys(value, keys);
for (let index = 0; index < keys.length; index += 1) {
if (!Object.prototype.hasOwnProperty.call(value, keys[index])) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + keys[index] + " is required");
}
}
const dimensions = previewRect(value.previewWidth, value.previewHeight, "expectedSnapshot preview frame");
return {
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
previewWidth: dimensions.width,
previewHeight: dimensions.height
};
}
function previewRect(width, height, name) {
return {
width: boundedInt(width, name + ".width", 16, 10240),
height: boundedInt(height, name + ".height", 16, 8192)
};
}
function boundedInt(value, name, minimum, maximum) {
if (!Number.isInteger(value) || value < minimum || value > maximum) {
throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " to " + maximum);
}
return value;
}
function requireConfirmation(value) {
if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "sequence.previewFrame.update requires confirmSetPreviewFrame: true after reviewing the complete snapshot");
}
function requireOperationId(value) {
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters");
}
function sameSnapshot(left, right) {
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
left.previewWidth === right.previewWidth && left.previewHeight === right.previewHeight;
}
function requiredGuid(value, name) {
const guid = guidString(value);
if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
return guid;
}
function guidString(value) {
if (value == null) return "";
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
}
function messageOf(error) { return error && error.message ? String(error.message) : String(error || "unknown error"); }
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return { createSequencePreviewFrameWorkflowDefinitions };
});
+333
View File
@@ -0,0 +1,333 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpSlideWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// A slide is a narrow three-item trim composition: it moves the center item
// without changing its source range, then retimes the immediate neighbours
// to retain both adjacent cuts. It intentionally does not ripple, relink, or
// infer source handles outside the exact readback below.
function createSlideWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const fallbackTails = new Map();
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
? deps.locks
: { withTrackMutationLock: localLock };
const definitions = {
"trackItem.slide.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTrackItemSlide,
handler: inspectTrackItemSlide
},
"trackItem.slide": {
destructive: true,
undoable: true,
idempotent: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTrackItemSlide,
handler: slideTrackItem
}
};
function canUseTrackItemSlide() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
}
async function inspectTrackItemSlide(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
return slideSnapshot(await activeTriplet(args, false));
}
async function slideTrackItem(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "slideBySeconds", "confirmSlide", "operationId"]);
requireConfirmation(args.confirmSlide);
requireOperationId(args.operationId);
const target = targetCoordinates(args);
const expected = requiredSnapshot(args.expectedSnapshot);
const offset = signedOffset(args.slideBySeconds);
assertExpectedTarget(expected, target);
const initial = await activeTriplet(target, true);
if (initial.projectGuid !== expected.projectGuid || initial.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed slide snapshot");
}
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
return locks.withTrackMutationLock(key, async function () {
// Read the complete affected triplet only after entering the shared
// track tail. A different operation ID cannot reuse an old snapshot
// after a preceding slide or source-only slip has committed.
const context = await activeTriplet(target, true);
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the slide transaction");
}
const before = await slideSnapshot(context);
if (!sameSnapshot(before, expected)) {
throw commandError("UXP_STALE_TRACK_ITEM", "The target or either immediate neighbour changed since the reviewed slide snapshot");
}
assertSlideSupported(before);
const desired = desiredSlide(before, offset);
let committed = false;
context.project.lockedAccess(function () {
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before slide action creation");
}
const actions = [
createAction(context.target, "createMoveAction", offset, "center move"),
createAction(context.previous, "createSetEndAction", desired.previous.endSeconds, "previous timeline end"),
createAction(context.previous, "createSetOutPointAction", desired.previous.outSeconds, "previous source out"),
createAction(context.following, "createSetStartAction", desired.following.startSeconds, "following timeline start"),
createAction(context.following, "createSetInPointAction", desired.following.inSeconds, "following source in")
];
committed = context.project.executeTransaction(function (compoundAction) {
for (const action of actions) {
if (compoundAction.addAction(action) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a slide action");
}
}
}, "Slide timeline item");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the slide transaction");
// Re-resolve all coordinates. A failed postcondition can follow a
// committed host transaction, so callers must inspect before another
// mutation if this verification throws.
const afterContext = await activeTriplet(target, true);
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed slide");
}
const after = await slideSnapshot(afterContext);
if (!sameSlideResult(before, after, desired)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested contiguous three-item slide");
}
return {
slid: true,
before,
after,
slideBySeconds: offset,
outcome: "verified",
verificationBoundary: "three_track_item_source_and_timeline_readback",
undoLabel: "Slide timeline item"
};
});
}
async function activeTriplet(args, requireTransaction) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
const target = targetCoordinates(args), projectGuid = requiredGuid(project.guid, "active project GUID"), sequenceId = requiredGuid(sequence.guid, "active sequence GUID");
const items = await clipItemsAt(sequence, target);
if (target.clipIndex < 1 || target.clipIndex + 1 >= items.length) {
throw commandError("UXP_TARGET_UNSUPPORTED", "A slide requires an immediate previous and following clip on the same track");
}
return {
project, sequence, projectGuid, sequenceId, ...target,
previous: items[target.clipIndex - 1], target: items[target.clipIndex], following: items[target.clipIndex + 1]
};
}
async function clipItemsAt(sequence, target) {
const title = target.mediaType === "video" ? "Video" : "Audio";
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
}
const count = Number(await sequence[countMethod]());
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
return items;
}
async function slideSnapshot(context) {
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
clipIndex: context.clipIndex,
previous: await itemSnapshot(context.previous, "previous"),
target: await itemSnapshot(context.target, "target"),
following: await itemSnapshot(context.following, "following")
};
}
async function itemSnapshot(item, label) {
const snapshot = {
startSeconds: tickSeconds(await requiredMethod(item, "getStartTime")()),
endSeconds: tickSeconds(await requiredMethod(item, "getEndTime")()),
inSeconds: tickSeconds(await requiredMethod(item, "getInPoint")()),
outSeconds: tickSeconds(await requiredMethod(item, "getOutPoint")()),
durationSeconds: tickSeconds(await requiredMethod(item, "getDuration")()),
speed: Number(await requiredMethod(item, "getSpeed")()),
reversed: Boolean(await requiredMethod(item, "isSpeedReversed")())
};
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " " + key);
}
}
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " speed");
}
return snapshot;
}
function assertSlideSupported(snapshot) {
for (const label of ["previous", "target", "following"]) {
const item = snapshot[label];
if (!numbersEqual(item.speed, 1) || item.reversed || !numbersEqual(item.endSeconds - item.startSeconds, item.durationSeconds) ||
!numbersEqual(item.outSeconds - item.inSeconds, item.durationSeconds) || item.durationSeconds <= 0) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Slide only supports contiguous forward 1x items with matching source and timeline durations");
}
}
if (!numbersEqual(snapshot.previous.endSeconds, snapshot.target.startSeconds) || !numbersEqual(snapshot.target.endSeconds, snapshot.following.startSeconds)) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Slide requires no gap or overlap at either adjacent cut");
}
}
function desiredSlide(before, offset) {
const previous = { ...before.previous, endSeconds: before.previous.endSeconds + offset, outSeconds: before.previous.outSeconds + offset, durationSeconds: before.previous.durationSeconds + offset };
const target = { ...before.target, startSeconds: before.target.startSeconds + offset, endSeconds: before.target.endSeconds + offset };
const following = { ...before.following, startSeconds: before.following.startSeconds + offset, inSeconds: before.following.inSeconds + offset, durationSeconds: before.following.durationSeconds - offset };
for (const item of [previous, target, following]) {
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds"]) {
if (!Number.isFinite(item[key]) || item[key] < 0 || item[key] > 86400) {
throw commandError("UXP_TARGET_UNSUPPORTED", "The requested slide exceeds documented timeline or source-time bounds");
}
}
}
if (previous.endSeconds <= previous.startSeconds || previous.outSeconds <= previous.inSeconds ||
following.endSeconds <= following.startSeconds || following.outSeconds <= following.inSeconds) {
throw commandError("UXP_TARGET_UNSUPPORTED", "The requested slide would create a zero- or negative-duration neighbour");
}
return { previous, target, following };
}
function sameSlideResult(before, after, desired) {
return sameIdentity(before, after) &&
sameItem(after.previous, desired.previous) && sameItem(after.target, desired.target) && sameItem(after.following, desired.following) &&
numbersEqual(after.previous.endSeconds, after.target.startSeconds) && numbersEqual(after.target.endSeconds, after.following.startSeconds) &&
numbersEqual(after.previous.endSeconds - after.previous.startSeconds, after.previous.durationSeconds) &&
numbersEqual(after.target.endSeconds - after.target.startSeconds, after.target.durationSeconds) &&
numbersEqual(after.following.endSeconds - after.following.startSeconds, after.following.durationSeconds) &&
numbersEqual(after.previous.outSeconds - after.previous.inSeconds, after.previous.durationSeconds) &&
numbersEqual(after.target.outSeconds - after.target.inSeconds, after.target.durationSeconds) &&
numbersEqual(after.following.outSeconds - after.following.inSeconds, after.following.durationSeconds);
}
function sameSnapshot(left, right) {
return sameIdentity(left, right) && sameItem(left.previous, right.previous) && sameItem(left.target, right.target) && sameItem(left.following, right.following);
}
function sameIdentity(left, right) {
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId && left.mediaType === right.mediaType &&
left.trackIndex === right.trackIndex && left.clipIndex === right.clipIndex;
}
function sameItem(left, right) {
return numbersEqual(left.startSeconds, right.startSeconds) && numbersEqual(left.endSeconds, right.endSeconds) &&
numbersEqual(left.inSeconds, right.inSeconds) && numbersEqual(left.outSeconds, right.outSeconds) &&
numbersEqual(left.durationSeconds, right.durationSeconds) && numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
}
function createAction(item, method, seconds, label) {
const action = requiredMethod(item, method)(ppro.TickTime.createWithSeconds(seconds));
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the " + label + " action");
return action;
}
function targetCoordinates(args) {
return { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"), clipIndex: nonNegativeInt(args.clipIndex, "clipIndex") };
}
function requiredSnapshot(value) {
assertObject(value, "expectedSnapshot");
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "previous", "target", "following"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
return {
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"), sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
previous: requiredItemSnapshot(value.previous, "expectedSnapshot.previous"), target: requiredItemSnapshot(value.target, "expectedSnapshot.target"), following: requiredItemSnapshot(value.following, "expectedSnapshot.following")
};
}
function requiredItemSnapshot(value, name) {
assertObject(value, name);
const allowed = ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
return {
startSeconds: boundedSeconds(value.startSeconds, name + ".startSeconds"), endSeconds: boundedSeconds(value.endSeconds, name + ".endSeconds"),
inSeconds: boundedSeconds(value.inSeconds, name + ".inSeconds"), outSeconds: boundedSeconds(value.outSeconds, name + ".outSeconds"),
durationSeconds: boundedSeconds(value.durationSeconds, name + ".durationSeconds"), speed: boundedSpeed(value.speed, name + ".speed"), reversed: requiredBoolean(value.reversed, name + ".reversed")
};
}
function assertExpectedTarget(expected, target) {
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
}
}
function signedOffset(value) {
const offset = Number(value);
if (!Number.isFinite(offset) || offset < -60 || offset > 60 || numbersEqual(offset, 0)) {
throw commandError("UXP_INVALID_ARGUMENT", "slideBySeconds must be a non-zero number from -60 to 60");
}
return offset;
}
function localLock(key, operation) {
const previous = fallbackTails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
fallbackTails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
});
}
function requiredMethod(target, name) { if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The timeline item does not expose " + name); return target[name].bind(target); }
function assertObject(value, name) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object"); }
function assertOnlyKeys(value, allowed) { assertObject(value, "arguments"); for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }
function enumValue(value, name, allowed) { if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
function nonNegativeInt(value, name) { if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511"); return value; }
function boundedSeconds(value, name) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400"); return seconds; }
function boundedSpeed(value, name) { const speed = Number(value); if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100"); return speed; }
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
function requireConfirmation(value) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "trackItem.slide requires confirmSlide: true"); }
function requireOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); }
function requiredGuid(value, name) { const guid = guidString(value); if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return guid; }
function guidString(value) { if (value == null) return ""; try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; } }
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
function numbersEqual(left, right) { return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001; }
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return definitions;
}
return { createSlideWorkflowDefinitions };
});
+362
View File
@@ -0,0 +1,362 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpSlipWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// A slip is deliberately a separate, narrow operation rather than a
// convenience alias for trackItem.update: it preserves timeline timing,
// requires the complete reviewed state, and serializes its whole guarded
// preflight/transaction/readback boundary per target.
function createSlipWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const fallbackTails = new Map();
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
? deps.locks
: { withTrackMutationLock: localLock };
const definitions = {
"trackItem.slip.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTrackItemSlip,
handler: inspectTrackItemSlip
},
"trackItem.slip": {
destructive: true,
undoable: true,
idempotent: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTrackItemSlip,
handler: slipTrackItem
}
};
function canUseTrackItemSlip() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
}
async function inspectTrackItemSlip(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
const context = await activeTarget(args, false);
return await slipSnapshot(context);
}
async function slipTrackItem(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "slipBySeconds", "confirmSlip", "operationId"]);
requireConfirmation(args.confirmSlip);
requireOperationId(args.operationId);
const target = targetCoordinates(args);
const expected = requiredSnapshot(args.expectedSnapshot);
const requestedOffset = signedOffset(args.slipBySeconds);
assertExpectedTarget(expected, target);
const initial = await activeTarget(target, true);
if (expected.projectGuid !== initial.projectGuid || expected.sequenceId !== initial.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed slip snapshot");
}
// Slides can trim either immediate neighbour, so slips and slides share
// a track-level lock rather than allowing different command families to
// pass one another with independently reviewed snapshots.
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
return locks.withTrackMutationLock(key, async function () {
// Resolve the active target only after entering the per-item tail so
// a different operation ID cannot reuse a snapshot taken before a
// prior slip completed.
const context = await activeTarget(target, true);
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the slip transaction");
}
const before = await slipSnapshot(context);
if (!sameSnapshot(before, expected)) {
throw commandError("UXP_STALE_TRACK_ITEM", "The timeline item changed since the reviewed slip snapshot");
}
assertSlipSupported(before);
const desired = desiredSlip(before, requestedOffset);
// The complete asynchronous snapshot is captured immediately before
// this synchronous action-creation boundary. The keyed tail prevents
// another MCP slip from interleaving between this check and readback.
let committed = false;
context.project.lockedAccess(function () {
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before slip action creation");
}
const inAction = createPointAction(context.item, "createSetInPointAction", desired.inSeconds, "source in point");
const outAction = createPointAction(context.item, "createSetOutPointAction", desired.outSeconds, "source out point");
committed = context.project.executeTransaction(function (compoundAction) {
if (compoundAction.addAction(inAction) === false || compoundAction.addAction(outAction) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a slip action");
}
}, "Slip timeline item source");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the slip transaction");
// Resolve by coordinate again rather than trusting the retained object.
// An action may have committed even when this verification fails, so
// callers must inspect before issuing another edit after that error.
const afterContext = await activeTarget(target, true);
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed slip");
}
const after = await slipSnapshot(afterContext);
if (!sameSlipResult(before, after, desired)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested source-only slip");
}
return {
slipped: true,
before,
after,
slipBySeconds: requestedOffset,
outcome: "verified",
verificationBoundary: "track_item_source_and_timeline_readback",
undoLabel: "Slip timeline item source"
};
});
}
async function activeTarget(args, requireTransaction) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
const projectGuid = requiredGuid(project.guid, "active project GUID"), sequenceId = requiredGuid(sequence.guid, "active sequence GUID");
const target = targetCoordinates(args), item = await trackItemAt(sequence, target);
return { project, sequence, item, projectGuid, sequenceId, ...target };
}
async function trackItemAt(sequence, target) {
const title = target.mediaType === "video" ? "Video" : "Audio";
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
}
const count = Number(await sequence[countMethod]());
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
const track = await sequence[trackMethod](target.trackIndex);
const itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
const item = items[target.clipIndex];
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
return item;
}
async function slipSnapshot(context) {
const snapshot = {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
clipIndex: context.clipIndex,
startSeconds: tickSeconds(await requiredMethod(context.item, "getStartTime")()),
endSeconds: tickSeconds(await requiredMethod(context.item, "getEndTime")()),
inSeconds: tickSeconds(await requiredMethod(context.item, "getInPoint")()),
outSeconds: tickSeconds(await requiredMethod(context.item, "getOutPoint")()),
durationSeconds: tickSeconds(await requiredMethod(context.item, "getDuration")()),
speed: Number(await requiredMethod(context.item, "getSpeed")()),
reversed: Boolean(await requiredMethod(context.item, "isSpeedReversed")())
};
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + key + " for the timeline item");
}
}
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid timeline-item speed");
}
return snapshot;
}
function desiredSlip(before, offset) {
const sourceDuration = before.outSeconds - before.inSeconds;
if (!numbersEqual(before.endSeconds - before.startSeconds, before.durationSeconds) ||
!numbersEqual(sourceDuration, before.durationSeconds) || sourceDuration <= 0) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Slip requires an unchanged forward 1x timeline and source duration");
}
const inSeconds = before.inSeconds + offset, outSeconds = before.outSeconds + offset;
if (inSeconds < 0 || outSeconds > 86400 || outSeconds <= inSeconds) {
throw commandError("UXP_TARGET_UNSUPPORTED", "The requested slip exceeds the documented source-time bounds");
}
return { inSeconds, outSeconds, sourceDuration };
}
function assertSlipSupported(snapshot) {
if (!numbersEqual(snapshot.speed, 1) || snapshot.reversed) {
throw commandError("UXP_TARGET_UNSUPPORTED", "Slip only supports forward 1x track items");
}
}
function sameSlipResult(before, after, desired) {
return sameSnapshotIdentity(before, after) &&
numbersEqual(after.startSeconds, before.startSeconds) &&
numbersEqual(after.endSeconds, before.endSeconds) &&
numbersEqual(after.durationSeconds, before.durationSeconds) &&
numbersEqual(after.speed, before.speed) && after.reversed === before.reversed &&
numbersEqual(after.inSeconds, desired.inSeconds) && numbersEqual(after.outSeconds, desired.outSeconds) &&
numbersEqual(after.outSeconds - after.inSeconds, desired.sourceDuration);
}
function sameSnapshot(left, right) {
return sameSnapshotIdentity(left, right) &&
numbersEqual(left.startSeconds, right.startSeconds) &&
numbersEqual(left.endSeconds, right.endSeconds) &&
numbersEqual(left.inSeconds, right.inSeconds) &&
numbersEqual(left.outSeconds, right.outSeconds) &&
numbersEqual(left.durationSeconds, right.durationSeconds) &&
numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
}
function sameSnapshotIdentity(left, right) {
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
left.mediaType === right.mediaType && left.trackIndex === right.trackIndex && left.clipIndex === right.clipIndex;
}
function localLock(key, operation) {
const previous = fallbackTails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
fallbackTails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
});
}
function createPointAction(item, method, seconds, label) {
const creator = requiredMethod(item, method);
const action = creator(ppro.TickTime.createWithSeconds(seconds));
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the " + label + " action");
return action;
}
function targetCoordinates(args) {
return {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
};
}
function requiredSnapshot(value) {
assertObject(value, "expectedSnapshot");
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
assertOnlyKeys(value, allowed);
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
return {
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
startSeconds: boundedSeconds(value.startSeconds, "expectedSnapshot.startSeconds"),
endSeconds: boundedSeconds(value.endSeconds, "expectedSnapshot.endSeconds"),
inSeconds: boundedSeconds(value.inSeconds, "expectedSnapshot.inSeconds"),
outSeconds: boundedSeconds(value.outSeconds, "expectedSnapshot.outSeconds"),
durationSeconds: boundedSeconds(value.durationSeconds, "expectedSnapshot.durationSeconds"),
speed: boundedSpeed(value.speed, "expectedSnapshot.speed"),
reversed: requiredBoolean(value.reversed, "expectedSnapshot.reversed")
};
}
function assertExpectedTarget(expected, target) {
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
}
}
function signedOffset(value) {
const offset = Number(value);
if (!Number.isFinite(offset) || offset < -60 || offset > 60 || numbersEqual(offset, 0)) {
throw commandError("UXP_INVALID_ARGUMENT", "slipBySeconds must be a non-zero number from -60 to 60");
}
return offset;
}
function requiredMethod(target, name) {
if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The timeline item does not expose " + name);
return target[name].bind(target);
}
function requiredGuid(value, name) {
const guid = guidString(value);
if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
return guid;
}
function guidString(value) {
if (value == null) return "";
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
}
function tickSeconds(value) {
const seconds = value && Number(value.seconds);
return Number.isFinite(seconds) ? seconds : null;
}
function numbersEqual(left, right) {
return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001;
}
return definitions;
}
function assertObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object");
}
function assertOnlyKeys(value, allowed) {
assertObject(value, "arguments");
for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key);
}
function enumValue(value, name, allowed) {
if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", "));
return value;
}
function nonNegativeInt(value, name) {
if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511");
return value;
}
function boundedSeconds(value, name) {
const seconds = Number(value);
if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400");
return seconds;
}
function boundedSpeed(value, name) {
const speed = Number(value);
if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100");
return speed;
}
function requiredBoolean(value, name) {
if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean");
return value;
}
function requireConfirmation(value) {
if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "confirmSlip must be true after reviewing the complete slip snapshot");
}
function requireOperationId(value) {
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be a 1-128 character operation token");
}
return value;
}
function commandError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { createSlipWorkflowDefinitions };
});
@@ -0,0 +1,204 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpSourceMediaProvenanceWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// This command intentionally resolves only the explicitly requested item
// twice. It must not invoke media-path getters while traversing unrelated
// project items because those values are sensitive and are outside the
// caller's bounded target.
function createSourceMediaProvenanceWorkflowDefinitions(deps) {
const ppro = deps.ppro;
return {
"source.provenance.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "26.3.0",
probe: canInspectSourceProvenance,
handler: inspectSourceProvenance
}
};
function canInspectSourceProvenance() {
return !!(
ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.FolderItem && typeof ppro.FolderItem.cast === "function" &&
ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function"
);
}
async function inspectSourceProvenance(args) {
const request = requestFrom(args);
const first = await sourceProvenanceSnapshot(request);
const second = await sourceProvenanceSnapshot(request);
if (!sameSnapshot(first, second)) {
throw commandError(
"UXP_STALE_SOURCE_PROVENANCE",
"The active project, requested project item, or requested provenance path changed while it was being inspected"
);
}
return second;
}
function requestFrom(args) {
assertOnlyKeys(args, ["projectItemId", "includeMediaFilePath", "includeOriginatingProjectPath"]);
const includeMediaFilePath = optionalBoolean(args.includeMediaFilePath, "includeMediaFilePath");
const includeOriginatingProjectPath = optionalBoolean(args.includeOriginatingProjectPath, "includeOriginatingProjectPath");
if (!includeMediaFilePath && !includeOriginatingProjectPath) {
throw commandError(
"UXP_PATH_DISCLOSURE_REQUIRED",
"Set includeMediaFilePath or includeOriginatingProjectPath to true before a native path is read"
);
}
return {
projectItemId: requiredText(args.projectItemId, "projectItemId", 512),
includeMediaFilePath,
includeOriginatingProjectPath
};
}
async function sourceProvenanceSnapshot(request) {
const project = await activeProject();
const projectGuid = requiredGuid(project.guid, "active project GUID");
const rootItem = await requiredMethod(project, "getRootItem")();
const projectItem = await findProjectItem(rootItem, request.projectItemId);
if (!projectItem) {
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId does not identify an item in the active project");
}
const resolvedId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
if (resolvedId !== request.projectItemId) {
throw commandError("UXP_STALE_SOURCE_PROVENANCE", "The resolved project item ID changed during provenance inspection");
}
const clipProjectItem = castClipProjectItem(projectItem);
const readbackId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
if (readbackId !== request.projectItemId) {
throw commandError("UXP_STALE_SOURCE_PROVENANCE", "The resolved project item changed before provenance getters ran");
}
const snapshot = { projectGuid, projectItemId: readbackId };
if (request.includeMediaFilePath) {
snapshot.mediaFilePath = boundedPath(await requiredMethod(clipProjectItem, "getMediaFilePath")(), "media file path");
}
if (request.includeOriginatingProjectPath) {
snapshot.originatingProjectPath = boundedPath(await requiredMethod(clipProjectItem, "getOriginatingProjectPath")(), "originating project path");
}
return snapshot;
}
async function activeProject() {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
return project;
}
async function findProjectItem(rootItem, expectedId) {
if (!rootItem || typeof rootItem !== "object") {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project root item");
}
const pending = [rootItem];
const visited = new Set();
while (pending.length) {
const candidate = pending.shift();
const candidateId = requiredText(requiredMethod(candidate, "getId")(), "project item ID", 512);
if (visited.has(candidateId)) continue;
visited.add(candidateId);
if (visited.size > 4096) {
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
}
if (candidateId === expectedId) return candidate;
const folder = castFolderItem(candidate);
if (!folder) continue;
const children = await requiredMethod(folder, "getItems")();
if (!Array.isArray(children)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project folder collection");
}
if (children.length > 4096 || pending.length + children.length + visited.size > 4096) {
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
}
for (let index = 0; index < children.length; index += 1) pending.push(children[index]);
}
return null;
}
function castFolderItem(projectItem) {
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere FolderItem APIs are unavailable");
}
try {
return ppro.FolderItem.cast(projectItem) || null;
} catch (_) {
return null;
}
}
function castClipProjectItem(projectItem) {
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere ClipProjectItem APIs are unavailable");
}
try {
const clip = ppro.ClipProjectItem.cast(projectItem);
if (clip) return clip;
} catch (_) {
// A non-clip project item cannot expose the two documented provenance
// getters. Preserve that distinction instead of treating it as absent.
}
throw commandError("UXP_TARGET_NOT_MEDIA", "projectItemId does not identify a media-backed ClipProjectItem");
}
}
function assertObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
}
}
function assertOnlyKeys(value, allowed) {
assertObject(value, "args");
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
}
function optionalBoolean(value, name) {
if (value === undefined) return false;
if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean");
return value;
}
function requiredMethod(value, method) {
if (!value || typeof value[method] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
}
return value[method].bind(value);
}
function requiredText(value, name, maximum) {
if (typeof value !== "string" || !value.length || value.length > maximum || value.indexOf("\u0000") !== -1) {
throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
}
return value;
}
function requiredGuid(value, name) {
let guid = "";
try { guid = value == null ? "" : typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { guid = ""; }
return requiredText(guid, name, 128);
}
function boundedPath(value, name) {
if (typeof value !== "string" || value.length > 4096 || value.indexOf("\u0000") !== -1) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name);
}
return value;
}
function sameSnapshot(left, right) {
return left.projectGuid === right.projectGuid &&
left.projectItemId === right.projectItemId &&
left.mediaFilePath === right.mediaFilePath &&
left.originatingProjectPath === right.originatingProjectPath;
}
function commandError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { createSourceMediaProvenanceWorkflowDefinitions };
});
@@ -0,0 +1,206 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpSourceProxyWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// Project-item traversal intentionally reads only identifiers and folder
// membership. Native proxy getters run only on the requested item, and the
// optional proxy path getter runs only when the caller explicitly asks for it.
function createSourceProxyWorkflowDefinitions(deps) {
const ppro = deps.ppro;
return {
"source.proxy.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "26.3.0",
probe: canInspectSourceProxy,
handler: inspectSourceProxy
}
};
function canInspectSourceProxy() {
return !!(
ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.FolderItem && typeof ppro.FolderItem.cast === "function" &&
ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function"
);
}
async function inspectSourceProxy(args) {
const request = requestFrom(args);
const first = await sourceProxySnapshot(request);
const second = await sourceProxySnapshot(request);
if (!sameSnapshot(first, second)) {
throw commandError(
"UXP_STALE_SOURCE_PROXY",
"The active project, requested project item, or requested proxy state changed while it was being inspected"
);
}
return second;
}
function requestFrom(args) {
assertOnlyKeys(args, ["projectItemId", "includeProxyPath"]);
return {
projectItemId: requiredText(args.projectItemId, "projectItemId", 512),
includeProxyPath: optionalBoolean(args.includeProxyPath, "includeProxyPath")
};
}
async function sourceProxySnapshot(request) {
const project = await activeProject();
const projectGuid = requiredGuid(project.guid, "active project GUID");
const rootItem = await requiredMethod(project, "getRootItem")();
const projectItem = await findProjectItem(rootItem, request.projectItemId);
if (!projectItem) {
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId does not identify an item in the active project");
}
const resolvedId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
if (resolvedId !== request.projectItemId) {
throw commandError("UXP_STALE_SOURCE_PROXY", "The resolved project item ID changed during proxy inspection");
}
const clipProjectItem = castClipProjectItem(projectItem);
const readbackId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
if (readbackId !== request.projectItemId) {
throw commandError("UXP_STALE_SOURCE_PROXY", "The resolved project item changed before proxy getters ran");
}
const snapshot = {
projectGuid,
projectItemId: readbackId,
canChangeMediaPath: requiredBoolean(await requiredMethod(clipProjectItem, "canChangeMediaPath")(), "canChangeMediaPath"),
isOffline: requiredBoolean(await requiredMethod(clipProjectItem, "isOffline")(), "isOffline"),
canProxy: requiredBoolean(await requiredMethod(clipProjectItem, "canProxy")(), "canProxy"),
hasProxy: requiredBoolean(await requiredMethod(clipProjectItem, "hasProxy")(), "hasProxy")
};
if (request.includeProxyPath && snapshot.hasProxy) {
snapshot.proxyPath = boundedPath(await requiredMethod(clipProjectItem, "getProxyPath")(), "proxy path");
}
return snapshot;
}
async function activeProject() {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
return project;
}
async function findProjectItem(rootItem, expectedId) {
if (!rootItem || typeof rootItem !== "object") {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project root item");
}
const pending = [rootItem];
const visited = new Set();
while (pending.length) {
const candidate = pending.shift();
const candidateId = requiredText(requiredMethod(candidate, "getId")(), "project item ID", 512);
if (visited.has(candidateId)) continue;
visited.add(candidateId);
if (visited.size > 4096) {
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
}
if (candidateId === expectedId) return candidate;
const folder = castFolderItem(candidate);
if (!folder) continue;
const children = await requiredMethod(folder, "getItems")();
if (!Array.isArray(children)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project folder collection");
}
if (children.length > 4096 || pending.length + children.length + visited.size > 4096) {
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
}
for (let index = 0; index < children.length; index += 1) pending.push(children[index]);
}
return null;
}
function castFolderItem(projectItem) {
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere FolderItem APIs are unavailable");
}
try {
return ppro.FolderItem.cast(projectItem) || null;
} catch (_) {
return null;
}
}
function castClipProjectItem(projectItem) {
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere ClipProjectItem APIs are unavailable");
}
try {
const clip = ppro.ClipProjectItem.cast(projectItem);
if (clip) return clip;
} catch (_) {
// A non-clip Project item does not expose the documented proxy getters.
}
throw commandError("UXP_TARGET_NOT_MEDIA", "projectItemId does not identify a media-backed ClipProjectItem");
}
}
function assertObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
}
}
function assertOnlyKeys(value, allowed) {
assertObject(value, "args");
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
}
function optionalBoolean(value, name) {
if (value === undefined) return false;
if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean");
return value;
}
function requiredMethod(value, method) {
if (!value || typeof value[method] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
}
return value[method].bind(value);
}
function requiredText(value, name, maximum) {
if (typeof value !== "string" || !value.length || value.length > maximum || value.indexOf("\u0000") !== -1) {
throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
}
return value;
}
function requiredGuid(value, name) {
let guid = "";
try { guid = value == null ? "" : typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { guid = ""; }
return requiredText(guid, name, 128);
}
function requiredBoolean(value, name) {
if (typeof value !== "boolean") {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name + " value");
}
return value;
}
function boundedPath(value, name) {
if (typeof value !== "string" || !value.length || value.length > 4096 || value.indexOf("\u0000") !== -1) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name);
}
return value;
}
function sameSnapshot(left, right) {
return left.projectGuid === right.projectGuid &&
left.projectItemId === right.projectItemId &&
left.canChangeMediaPath === right.canChangeMediaPath &&
left.isOffline === right.isOffline &&
left.canProxy === right.canProxy &&
left.hasProxy === right.hasProxy &&
left.proxyPath === right.proxyPath;
}
function commandError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { createSourceProxyWorkflowDefinitions };
});
+48
View File
@@ -0,0 +1,48 @@
body {
color: #ddd;
background: #242424;
font: 12px system-ui;
margin: 14px;
}
button,
input {
margin: 4px;
padding: 6px;
}
button {
color: white;
background: #5b5bd6;
border: 1px solid transparent;
border-radius: 3px;
}
pre {
white-space: pre-wrap;
background: #181818;
padding: 10px;
}
label {
display: block;
}
.help {
color: #bbb;
line-height: 1.4;
}
button:focus-visible,
input:focus-visible {
outline: 2px solid #c7b8ff;
outline-offset: 2px;
}
@media (forced-colors: active) {
button,
input,
pre {
border: 1px solid CanvasText;
}
}
@@ -0,0 +1,102 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpTickTimeArithmeticWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// Keep this utility intentionally separate from frame alignment. It accepts
// already-quantized Premiere ticks and returns only TickTime's native value
// readback; it does not create a FrameRate, accept seconds, or inspect a
// sequence, track, clip, or project.
function createTickTimeArithmeticWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const definitions = {
"time.tickArithmetic.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTickArithmetic,
handler: inspectTickArithmetic
}
};
function canUseTickArithmetic() {
return !!(ppro && ppro.TickTime && typeof ppro.TickTime.createWithTicks === "function");
}
async function inspectTickArithmetic(args) {
assertOnlyKeys(args, ["operation", "baseTicks", "operandTicks", "factor"]);
const operation = enumValue(args.operation, "operation", ["add", "subtract", "multiply", "divide"]);
const baseTicks = canonicalTicks(args.baseTicks, "baseTicks");
const base = createTickTime(baseTicks, "baseTicks");
let operand = null, result;
if (operation === "add" || operation === "subtract") {
if (args.factor !== undefined) throw commandError("UXP_INVALID_ARGUMENT", "factor is only allowed for multiply or divide");
operand = createTickTime(canonicalTicks(args.operandTicks, "operandTicks"), "operandTicks");
const method = operation === "add" ? "add" : "subtract";
if (typeof base.value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose TickTime." + method);
result = base.value[method](operand.value);
} else {
if (args.operandTicks !== undefined) throw commandError("UXP_INVALID_ARGUMENT", "operandTicks is only allowed for add or subtract");
const factor = boundedFactor(args.factor, operation === "divide" ? "divisor" : "factor");
const method = operation === "multiply" ? "multiply" : "divide";
if (typeof base.value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose TickTime." + method);
result = base.value[method](factor);
operand = { factor };
}
return {
operation,
base: readTickTime(base.value, "base"),
...(operation === "add" || operation === "subtract" ? { operand: readTickTime(operand.value, "operand") } : { factor: operand.factor }),
result: readTickTime(result, "result"),
verificationBoundary: "native_tick_time_value_readback",
limitations: ["This is pure native TickTime arithmetic over caller-supplied ticks; it does not align frames, infer timecode, inspect Premiere project state, or prove a licensed host."]
};
}
function createTickTime(ticks, name) {
let value;
try { value = ppro.TickTime.createWithTicks(ticks); } catch (_) { value = null; }
if (!value) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere rejected " + name + " while constructing TickTime");
return { value };
}
function readTickTime(value, name) {
if (!value || typeof value !== "object") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned no TickTime for " + name);
const ticks = canonicalTicks(value.ticks, name + ".ticks");
const seconds = Number(value.seconds);
if (!Number.isFinite(seconds) || Math.abs(seconds) > 1_000_000_000) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned invalid " + name + ".seconds");
}
return { ticks, seconds };
}
function canonicalTicks(value, name) {
if (typeof value !== "string" || !/^(?:0|-?[1-9][0-9]{0,17})$/.test(value)) {
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a canonical signed tick integer with at most 18 digits");
}
return value;
}
function boundedFactor(value, name) {
if (!Number.isInteger(value) || value < -1000000 || value > 1000000 || value === 0) {
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-zero integer from -1000000 through 1000000");
}
return value;
}
function enumValue(value, name, allowed) {
if (!allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", "));
return value;
}
function assertOnlyKeys(value, allowed) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "arguments must be an object");
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
}
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return definitions;
}
return { createTickTimeArithmeticWorkflowDefinitions };
});
@@ -0,0 +1,306 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpTimelineSourceLabelWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// This is deliberately a source-project-item label action resolved from one
// active timeline coordinate. It does not claim to label a timeline-only
// instance: Premiere's documented color-label action belongs to the source
// ClipProjectItem, so other uses of that source can reflect the change.
function createTimelineSourceLabelWorkflowDefinitions(deps) {
const ppro = deps.ppro;
const fallbackTails = new Map();
const locks = deps.colorLabelLocks && typeof deps.colorLabelLocks.withProjectItemColorLabelLock === "function"
? deps.colorLabelLocks
: { withProjectItemColorLabelLock: localLock };
const definitions = {
"timeline.sourceLabel.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTimelineSourceLabels,
handler: inspectTimelineSourceLabel
},
"timeline.sourceLabel.update": {
destructive: true,
undoable: true,
idempotent: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canUseTimelineSourceLabels,
handler: updateTimelineSourceLabel
}
};
function canUseTimelineSourceLabels() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.Constants && ppro.Constants.TrackItemType && ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function");
}
async function inspectTimelineSourceLabel(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
return sourceLabelSnapshot(await activeTarget(args, false));
}
async function updateTimelineSourceLabel(args) {
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "colorIndex", "expectedSnapshot", "confirmSetLabel", "operationId"]);
requireConfirmation(args.confirmSetLabel);
requireOperationId(args.operationId);
const target = targetCoordinates(args), colorIndex = labelColorIndex(args.colorIndex);
const expected = requiredSnapshot(args.expectedSnapshot);
assertExpectedTarget(expected, target);
const initial = await activeTarget(target, true);
const initialSnapshot = await sourceLabelSnapshot(initial);
if (initialSnapshot.projectGuid !== expected.projectGuid || initialSnapshot.sequenceId !== expected.sequenceId ||
initialSnapshot.sourceProjectItemId !== expected.sourceProjectItemId) {
throw commandError("UXP_STALE_SOURCE_LABEL", "The active project, sequence, or timeline source item no longer matches the reviewed label snapshot");
}
const key = initialSnapshot.projectGuid + "\u0000" + initialSnapshot.sourceProjectItemId;
return locks.withProjectItemColorLabelLock(key, async function () {
// Re-resolve after joining the source-global tail. This catches both a
// competing coordinate update and the generic project-bin color tool,
// which uses the same lock for the same ClipProjectItem.
let context;
try {
context = await activeTarget(target, true);
} catch (error) {
if (error && error.code === "UXP_TARGET_NOT_FOUND") {
throw commandError("UXP_STALE_SOURCE_LABEL", "The reviewed timeline coordinate no longer resolves before color-label action creation");
}
throw error;
}
const before = await sourceLabelSnapshot(context);
if (!sameSnapshot(before, expected)) {
throw commandError("UXP_STALE_SOURCE_LABEL", "The reviewed timeline source label changed since inspection");
}
let committed = false;
context.project.lockedAccess(function () {
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
throw commandError("UXP_STALE_SOURCE_LABEL", "The active project or sequence changed before color-label action creation");
}
if (typeof context.source.createSetColorLabelAction !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This timeline source item cannot create a documented color-label action");
}
const action = context.source.createSetColorLabelAction(colorIndex);
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the source color-label action");
committed = context.project.executeTransaction(function (compoundAction) {
if (!compoundAction || typeof compoundAction.addAction !== "function" || compoundAction.addAction(action) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the source color-label action");
}
}, "Set timeline source label");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the source color-label transaction");
const after = await sourceLabelSnapshot(await activeTarget(target, true));
if (!sameTargetAfterUpdate(before, after, colorIndex)) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested source color label at the reviewed timeline coordinate");
}
return {
sourceLabelUpdated: true,
before,
after,
outcome: "verified",
verificationBoundary: "timeline_coordinate_source_color_label_readback",
undoLabel: "Set timeline source label"
};
});
}
async function activeTarget(args, requireTransaction) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
const target = targetCoordinates(args), items = await clipItemsAt(sequence, target);
const item = items[target.clipIndex];
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
const sourceItem = await requiredMethod(item, "getProjectItem")();
let source;
try { source = ppro.ClipProjectItem.cast(sourceItem); } catch (_) { source = null; }
if (!source) throw commandError("UXP_TARGET_UNSUPPORTED", "The resolved timeline item has no ClipProjectItem color-label surface");
const sourceProjectItemId = requiredIdentifier(await requiredMethod(source, "getId")(), "source project-item ID");
return {
project,
sequence,
projectGuid: requiredGuid(project.guid, "active project GUID"),
sequenceId: requiredGuid(sequence.guid, "active sequence GUID"),
source,
sourceProjectItemId,
trackItem: item,
trackItemCount: items.length,
...target
};
}
async function clipItemsAt(sequence, target) {
const title = target.mediaType === "video" ? "Video" : "Audio";
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
}
const count = Number(await sequence[countMethod]());
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (!items.length) throw commandError("UXP_TARGET_NOT_FOUND", "The target track has no clip items");
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
return items;
}
async function sourceLabelSnapshot(context) {
const colorLabelIndex = Number(await requiredMethod(context.source, "getColorLabelIndex")());
if (!Number.isSafeInteger(colorLabelIndex) || colorLabelIndex < 0 || colorLabelIndex > 15) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid source color-label index");
}
const startSeconds = tickSeconds(await requiredMethod(context.trackItem, "getStartTime")());
const endSeconds = tickSeconds(await requiredMethod(context.trackItem, "getEndTime")());
if (!validSeconds(startSeconds) || !validSeconds(endSeconds) || endSeconds < startSeconds) {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned invalid timeline coordinates for the source-label target");
}
return {
projectGuid: context.projectGuid,
sequenceId: context.sequenceId,
mediaType: context.mediaType,
trackIndex: context.trackIndex,
clipIndex: context.clipIndex,
trackItemCount: context.trackItemCount,
sourceProjectItemId: context.sourceProjectItemId,
sourceColorLabelIndex: colorLabelIndex,
startSeconds,
endSeconds
};
}
function targetCoordinates(args) {
return {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: boundedInt(args.trackIndex, "trackIndex", 0, 511),
clipIndex: boundedInt(args.clipIndex, "clipIndex", 0, 511)
};
}
function requiredSnapshot(value) {
assertObject(value, "expectedSnapshot");
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "trackItemCount", "sourceProjectItemId", "sourceColorLabelIndex", "startSeconds", "endSeconds"];
assertOnlyKeys(value, allowed);
for (let index = 0; index < allowed.length; index += 1) {
if (!Object.prototype.hasOwnProperty.call(value, allowed[index])) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + allowed[index] + " is required");
}
}
const snapshot = {
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
trackIndex: boundedInt(value.trackIndex, "expectedSnapshot.trackIndex", 0, 511),
clipIndex: boundedInt(value.clipIndex, "expectedSnapshot.clipIndex", 0, 511),
trackItemCount: boundedInt(value.trackItemCount, "expectedSnapshot.trackItemCount", 1, 512),
sourceProjectItemId: requiredIdentifier(value.sourceProjectItemId, "expectedSnapshot.sourceProjectItemId"),
sourceColorLabelIndex: labelColorIndex(value.sourceColorLabelIndex),
startSeconds: boundedSeconds(value.startSeconds, "expectedSnapshot.startSeconds"),
endSeconds: boundedSeconds(value.endSeconds, "expectedSnapshot.endSeconds")
};
if (snapshot.endSeconds < snapshot.startSeconds) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot.endSeconds must not precede startSeconds");
return snapshot;
}
function assertExpectedTarget(expected, target) {
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot coordinates must exactly match the requested timeline target");
}
}
function sameSnapshot(left, right) {
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId && left.mediaType === right.mediaType &&
left.trackIndex === right.trackIndex && left.clipIndex === right.clipIndex && left.trackItemCount === right.trackItemCount &&
left.sourceProjectItemId === right.sourceProjectItemId && left.sourceColorLabelIndex === right.sourceColorLabelIndex &&
sameNumber(left.startSeconds, right.startSeconds) && sameNumber(left.endSeconds, right.endSeconds);
}
function sameTargetAfterUpdate(before, after, colorIndex) {
return before.projectGuid === after.projectGuid && before.sequenceId === after.sequenceId && before.mediaType === after.mediaType &&
before.trackIndex === after.trackIndex && before.clipIndex === after.clipIndex && before.trackItemCount === after.trackItemCount &&
before.sourceProjectItemId === after.sourceProjectItemId && after.sourceColorLabelIndex === colorIndex &&
sameNumber(before.startSeconds, after.startSeconds) && sameNumber(before.endSeconds, after.endSeconds);
}
function localLock(key, operation) {
const previous = fallbackTails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
fallbackTails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
});
}
return definitions;
}
function assertObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
}
function assertOnlyKeys(value, allowed) {
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
}
function requiredMethod(value, method) {
if (!value || typeof value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
return value[method].bind(value);
}
function enumValue(value, name, allowed) {
if (!allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", "));
return value;
}
function boundedInt(value, name, minimum, maximum) {
if (!Number.isInteger(value) || value < minimum || value > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " to " + maximum);
return value;
}
function labelColorIndex(value) { return boundedInt(value, "colorIndex", 0, 15); }
function boundedSeconds(value, name) {
const seconds = Number(value);
if (!validSeconds(seconds)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400");
return seconds;
}
function validSeconds(value) { return Number.isFinite(value) && value >= 0 && value <= 86400; }
function sameNumber(left, right) { return Math.abs(Number(left) - Number(right)) <= 0.000001; }
function requireConfirmation(value) {
if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "timeline.sourceLabel.update requires confirmSetLabel: true after reviewing the complete snapshot");
}
function requireOperationId(value) {
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters");
}
function requiredGuid(value, name) {
const guid = guidString(value);
if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
return guid;
}
function requiredIdentifier(value, name) {
const id = guidString(value);
if (!id || id.length > 512 || id.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
return id;
}
function guidString(value) {
if (value == null) return "";
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
}
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return { createTimelineSourceLabelWorkflowDefinitions };
});
@@ -0,0 +1,30 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpTrackItemMutationLocks = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
// A slide modifies the target plus both immediate neighbours. Use the same
// track-level tail for slips and slides so an operation on one of those
// neighbours cannot bypass the reviewed snapshot while a slide is pending.
function createTrackItemMutationLocks() {
const tails = new Map();
function withTrackMutationLock(key, operation) {
const previous = tails.get(key) || Promise.resolve();
let release;
const gate = new Promise(function (resolve) { release = resolve; });
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
tails.set(key, tail);
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
release();
if (tails.get(key) === tail) tails.delete(key);
});
}
return { withTrackMutationLock };
}
return { createTrackItemMutationLocks };
});
+269
View File
@@ -0,0 +1,269 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpTranscriptImport = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const MAX_IMPORT_BYTES = 24 * 1024;
const MAX_SNAPSHOT_BYTES = 1024 * 1024;
const MAX_PROJECT_ITEMS = 512;
const MAX_PROJECT_DEPTH = 16;
function createTranscriptImportRuntime(deps) {
const ppro = deps.ppro, transcript = deps.TranscriptSupport, Protocol = deps.Protocol;
const importTails = new Map();
function commandError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function assertObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "args must be an object");
}
function assertOnlyKeys(value, allowed) {
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
}
function requiredString(value, name, maximum) {
if (typeof value !== "string" || !value.trim() || value.length > maximum) {
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-empty string of at most " + maximum + " characters");
}
return value;
}
function exactGuid(value, name) {
const guid = requiredString(value, name, 512);
if (guid === "[object Object]") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a readable GUID string");
return guid;
}
function expectedRevision(value) {
if (value === null) return null;
if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value)) {
throw commandError("UXP_INVALID_ARGUMENT", "expectedTranscriptRevision must be a sha256 revision or null");
}
return value;
}
function validateImportArgs(args) {
assertObject(args);
assertOnlyKeys(args, ["projectItemId", "expectedProjectGuid", "expectedTranscriptRevision", "json", "confirmDestructive", "operationId"]);
if (args.confirmDestructive !== true) {
throw commandError("UXP_CONFIRMATION_REQUIRED", "confirmDestructive: true is required to replace a source transcript");
}
if (typeof args.operationId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(args.operationId)) {
throw commandError("UXP_INVALID_ARGUMENT", "operationId must be 1-128 safe characters");
}
const json = requiredString(args.json, "json", MAX_IMPORT_BYTES);
if (transcript.utf8ByteLength(json) > MAX_IMPORT_BYTES) {
throw commandError("UXP_INVALID_ARGUMENT", "json exceeds the 24 KiB transcript-import bridge limit");
}
try { transcript.parseTranscriptJSON(json); } catch (error) {
throw commandError("UXP_INVALID_ARGUMENT", error && error.message ? error.message : "json is not valid transcript JSON");
}
return {
projectItemId: requiredString(args.projectItemId, "projectItemId", 512),
expectedProjectGuid: exactGuid(args.expectedProjectGuid, "expectedProjectGuid"),
expectedTranscriptRevision: expectedRevision(args.expectedTranscriptRevision),
json,
};
}
function guidString(value) {
if (value == null) return "";
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
}
async function projectItemId(item) {
if (!item || typeof item.getId !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Project-item identity access is unavailable");
const value = await item.getId();
if (value == null || String(value) === "") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an unreadable project-item ID");
return String(value);
}
function folderValue(item) {
if (!item || typeof item.getItems !== "function") return null;
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") return item;
try { return ppro.FolderItem.cast(item) || null; } catch (_) { return null; }
}
function clipValue(item) {
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "ClipProjectItem casting is unavailable");
}
try { return ppro.ClipProjectItem.cast(item) || null; } catch (_) { return null; }
}
async function resolveTarget(input) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Active-project access is unavailable");
}
const project = await ppro.Project.getActiveProject();
if (!project || typeof project.getRootItem !== "function") throw commandError("UXP_NO_ACTIVE_PROJECT", "No readable active project");
const projectGuid = guidString(project.guid);
if (!projectGuid || projectGuid === "[object Object]") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an unreadable active-project GUID");
if (projectGuid !== input.expectedProjectGuid) throw commandError("UXP_STALE_TRANSCRIPT", "The active project no longer matches expectedProjectGuid");
const root = await project.getRootItem();
const queue = [{ item: root, depth: 0 }];
let inspected = 0;
while (queue.length) {
const current = queue.shift(), folder = folderValue(current.item);
if (!folder) continue;
const children = Array.from(await folder.getItems() || []);
for (let index = 0; index < children.length; index += 1) {
inspected += 1;
if (inspected > MAX_PROJECT_ITEMS) {
throw commandError("UXP_PROJECT_TOO_LARGE", "Transcript target lookup exceeds " + MAX_PROJECT_ITEMS + " project items");
}
const child = children[index], childId = await projectItemId(child);
if (childId === input.projectItemId) {
const clip = clipValue(child);
if (!clip) throw commandError("UXP_TARGET_NOT_FOUND", "The requested project item is not a media clip");
return { project, projectGuid, clip, projectItemId: childId };
}
const childFolder = folderValue(child);
if (childFolder) {
if (current.depth >= MAX_PROJECT_DEPTH) {
throw commandError("UXP_PROJECT_TOO_LARGE", "Transcript target lookup exceeds project-folder depth " + MAX_PROJECT_DEPTH);
}
queue.push({ item: childFolder, depth: current.depth + 1 });
}
}
}
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId was not found in the active project");
}
async function transcriptSnapshot(clip) {
let hasTranscript = ppro.Transcript.hasTranscript(clip);
if (hasTranscript && typeof hasTranscript.then === "function") hasTranscript = await hasTranscript;
if (!hasTranscript) return { hasTranscript: false, transcriptRevision: null };
const json = await ppro.Transcript.exportToJSON(clip);
if (typeof json !== "string" || !json) throw commandError("UXP_VERIFICATION_FAILED", "Premiere reports a transcript but did not export readable JSON");
if (transcript.utf8ByteLength(json) > MAX_SNAPSHOT_BYTES) {
throw commandError("UXP_RESULT_TOO_LARGE", "Transcript readback exceeds the 1 MiB bounded snapshot limit");
}
try { transcript.parseTranscriptJSON(json); } catch (error) {
throw commandError("UXP_VERIFICATION_FAILED", error && error.message ? error.message : "Premiere returned invalid transcript JSON");
}
return { hasTranscript: true, transcriptRevision: transcript.transcriptRevision(json) };
}
function requireExpectedSnapshot(snapshot, expected) {
if (!snapshot.hasTranscript && expected === null) return;
if (snapshot.hasTranscript && snapshot.transcriptRevision === expected) return;
throw commandError("UXP_STALE_TRANSCRIPT", "The source transcript no longer matches expectedTranscriptRevision");
}
function serialize(key, work) {
const previous = importTails.get(key) || Promise.resolve();
const next = previous.catch(function () { return undefined; }).then(work);
importTails.set(key, next);
return next.finally(function () { if (importTails.get(key) === next) importTails.delete(key); });
}
function operation(verified, boundary, evidence) {
if (!Protocol || typeof Protocol.operationSemantics !== "function") return undefined;
return Protocol.operationSemantics({
mutatesProject: true,
verificationStatus: verified ? "verified" : "committed_unverified",
verificationBoundary: boundary,
verificationEvidence: evidence,
undoSupported: true,
undoLabel: "Import transcript",
transactionActionGroup: true,
cancellationSupported: true
});
}
async function importTranscript(args) {
const input = validateImportArgs(args);
return serialize(input.expectedProjectGuid + ":" + input.projectItemId, async function () {
const beforeTarget = await resolveTarget(input), before = await transcriptSnapshot(beforeTarget.clip);
requireExpectedSnapshot(before, input.expectedTranscriptRevision);
// Resolve and snapshot a second time immediately before action creation.
// The per-owner queue prevents a different operation ID in this panel
// from passing the same stale preflight concurrently.
const actionTarget = await resolveTarget(input), actionSnapshot = await transcriptSnapshot(actionTarget.clip);
requireExpectedSnapshot(actionSnapshot, input.expectedTranscriptRevision);
let textSegments;
try { textSegments = ppro.Transcript.importFromJSON(input.json); } catch (error) {
throw commandError("UXP_INVALID_ARGUMENT", error && error.message ? error.message : "Premiere rejected transcript JSON");
}
if (!textSegments) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create transcript text segments");
if (typeof actionTarget.project.lockedAccess !== "function" || typeof actionTarget.project.executeTransaction !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere transaction APIs are unavailable for transcript import");
}
let committed = false;
actionTarget.project.lockedAccess(function () {
const action = ppro.Transcript.createImportTextSegmentsAction(textSegments, actionTarget.clip);
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create a transcript import action");
committed = actionTarget.project.executeTransaction(function (compoundAction) {
if (!compoundAction || typeof compoundAction.addAction !== "function" || compoundAction.addAction(action) === false) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the transcript import action");
}
}, "Import transcript");
});
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the transcript import transaction");
const requestedRevision = transcript.transcriptRevision(input.json);
try {
const afterTarget = await resolveTarget(input), after = await transcriptSnapshot(afterTarget.clip);
const verified = after.hasTranscript && after.transcriptRevision === requestedRevision;
return {
committed: true,
verified,
projectGuid: actionTarget.projectGuid,
projectItemId: actionTarget.projectItemId,
before,
requestedTranscriptRevision: requestedRevision,
after,
outcome: verified ? "verified" : "committed_unverified",
verificationBoundary: verified ? "transcript_export_exact_readback" : "transcript_transaction_commit_with_readback_mismatch",
undoLabel: "Import transcript",
operation: operation(
verified,
verified ? "transcript_export_exact_readback" : "transcript_transaction_commit_with_readback_mismatch",
verified
? [{ type: "transcript_export_sha256", expected: requestedRevision, actual: after.transcriptRevision }]
: [{ type: "transcript_export_sha256", expected: requestedRevision, actual: after.transcriptRevision }]
)
};
} catch (error) {
return {
committed: true,
verified: false,
projectGuid: actionTarget.projectGuid,
projectItemId: actionTarget.projectItemId,
before,
requestedTranscriptRevision: requestedRevision,
after: null,
outcome: "committed_unverified",
verificationBoundary: "transcript_transaction_commit_with_readback_unavailable",
readbackError: error && error.message ? error.message : String(error),
undoLabel: "Import transcript",
operation: operation(false, "transcript_transaction_commit_with_readback_unavailable", [{
type: "readback_error", message: error && error.message ? error.message : String(error)
}])
};
}
});
}
function canImportTranscript() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" && ppro.Transcript
&& typeof ppro.Transcript.hasTranscript === "function" && typeof ppro.Transcript.exportToJSON === "function"
&& typeof ppro.Transcript.importFromJSON === "function" && typeof ppro.Transcript.createImportTextSegmentsAction === "function"
&& ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function");
}
return { canImportTranscript, importTranscript, constants: { MAX_IMPORT_BYTES, MAX_SNAPSHOT_BYTES, MAX_PROJECT_ITEMS, MAX_PROJECT_DEPTH } };
}
return { createTranscriptImportRuntime };
});
+185
View File
@@ -0,0 +1,185 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpTranscript = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const MAX_TRANSCRIPT_JSON_BYTES = 5 * 1024 * 1024;
function utf8ByteLength(value) {
let bytes = 0;
const text = String(value);
for (let index = 0; index < text.length; index += 1) {
const code = text.charCodeAt(index);
if (code < 0x80) bytes += 1;
else if (code < 0x800) bytes += 2;
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length && text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
bytes += 4;
index += 1;
} else bytes += 3;
}
return bytes;
}
// This stays local to the panel instead of assuming Web Crypto is exposed by
// every supported Premiere UXP runtime. It uses the same UTF-8 replacement
// behavior as Node's sha256 revision returned by get_clip_transcript_uxp.
function utf8Bytes(value) {
const text = String(value), bytes = [];
for (let index = 0; index < text.length; index += 1) {
let code = text.charCodeAt(index);
if (code >= 0xd800 && code <= 0xdbff) {
const next = text.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
code = 0x10000 + ((code - 0xd800) << 10) + next - 0xdc00;
index += 1;
} else code = 0xfffd;
} else if (code >= 0xdc00 && code <= 0xdfff) code = 0xfffd;
if (code < 0x80) bytes.push(code);
else if (code < 0x800) bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
else if (code < 0x10000) bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
else bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
}
return bytes;
}
function rightRotate(value, bits) { return (value >>> bits) | (value << (32 - bits)); }
function sha256Hex(value) {
const bytes = utf8Bytes(value), bitLength = bytes.length * 8;
bytes.push(0x80);
while ((bytes.length % 64) !== 56) bytes.push(0);
const high = Math.floor(bitLength / 0x100000000), low = bitLength >>> 0;
bytes.push((high >>> 24) & 0xff, (high >>> 16) & 0xff, (high >>> 8) & 0xff, high & 0xff);
bytes.push((low >>> 24) & 0xff, (low >>> 16) & 0xff, (low >>> 8) & 0xff, low & 0xff);
const words = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
];
const constants = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
for (let offset = 0; offset < bytes.length; offset += 64) {
const schedule = new Array(64);
for (let index = 0; index < 16; index += 1) {
const position = offset + index * 4;
schedule[index] = ((bytes[position] << 24) | (bytes[position + 1] << 16) | (bytes[position + 2] << 8) | bytes[position + 3]) >>> 0;
}
for (let index = 16; index < 64; index += 1) {
const previous = schedule[index - 15], earlier = schedule[index - 2];
const sigma0 = rightRotate(previous, 7) ^ rightRotate(previous, 18) ^ (previous >>> 3);
const sigma1 = rightRotate(earlier, 17) ^ rightRotate(earlier, 19) ^ (earlier >>> 10);
schedule[index] = (schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1) >>> 0;
}
let a = words[0], b = words[1], c = words[2], d = words[3], e = words[4], f = words[5], g = words[6], h = words[7];
for (let index = 0; index < 64; index += 1) {
const sigma1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
const choice = (e & f) ^ (~e & g);
const first = (h + sigma1 + choice + constants[index] + schedule[index]) >>> 0;
const sigma0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
const majority = (a & b) ^ (a & c) ^ (b & c);
const second = (sigma0 + majority) >>> 0;
h = g; g = f; f = e; e = (d + first) >>> 0; d = c; c = b; b = a; a = (first + second) >>> 0;
}
words[0] = (words[0] + a) >>> 0; words[1] = (words[1] + b) >>> 0;
words[2] = (words[2] + c) >>> 0; words[3] = (words[3] + d) >>> 0;
words[4] = (words[4] + e) >>> 0; words[5] = (words[5] + f) >>> 0;
words[6] = (words[6] + g) >>> 0; words[7] = (words[7] + h) >>> 0;
}
return words.map(function (word) { return (word >>> 0).toString(16).padStart(8, "0"); }).join("");
}
function transcriptRevision(raw) { return "sha256:" + sha256Hex(raw); }
function parseTranscriptJSON(raw) {
if (typeof raw !== "string" || raw.length === 0) throw new Error("transcript JSON must be a non-empty string");
if (utf8ByteLength(raw) > MAX_TRANSCRIPT_JSON_BYTES) throw new Error("transcript JSON exceeds the 5 MB command limit");
try { return JSON.parse(raw); } catch (error) { throw new Error("transcript JSON is invalid: " + error.message); }
}
function searchTranscriptJSON(raw, query, options) {
const document = parseTranscriptJSON(raw);
const term = typeof query === "string" ? query.trim() : "";
if (!term) throw new Error("query must be a non-empty string");
const settings = options || {};
const caseSensitive = settings.caseSensitive === true;
const requestedLimit = Number(settings.maxResults == null ? 50 : settings.maxResults);
if (!Number.isInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > 500) {
throw new Error("maxResults must be an integer between 1 and 500");
}
const needle = caseSensitive ? term : term.toLocaleLowerCase();
const matches = [];
const collectionLimit = requestedLimit + 1;
function visit(value, path) {
if (matches.length >= collectionLimit) return;
if (typeof value === "string") {
const haystack = caseSensitive ? value : value.toLocaleLowerCase();
let from = 0;
while (matches.length < collectionLimit) {
const index = haystack.indexOf(needle, from);
if (index < 0) break;
const contextStart = Math.max(0, index - 80);
const contextEnd = Math.min(value.length, index + term.length + 80);
matches.push({ path, index, text: value, context: value.slice(contextStart, contextEnd) });
from = index + Math.max(needle.length, 1);
}
} else if (Array.isArray(value)) {
for (let i = 0; i < value.length && matches.length < collectionLimit; i += 1) visit(value[i], path + "[" + i + "]");
} else if (value && typeof value === "object") {
const keys = Object.keys(value);
for (let i = 0; i < keys.length && matches.length < collectionLimit; i += 1) {
const key = keys[i];
visit(value[key], path ? path + "." + key : key);
}
}
}
visit(document, "$");
return { query: term, caseSensitive, matches: matches.slice(0, requestedLimit), limited: matches.length > requestedLimit };
}
function versionAtLeast(version, minimum) {
const current = String(version || "").split(".").map(Number);
const required = String(minimum).split(".").map(Number);
for (let i = 0; i < Math.max(current.length, required.length); i += 1) {
const left = Number.isFinite(current[i]) ? current[i] : 0;
const right = Number.isFinite(required[i]) ? required[i] : 0;
if (left !== right) return left > right;
}
return true;
}
function matchingClipCandidate(item, itemId, wantedId, wantedName, cast) {
const idMatch = !!wantedId && itemId === wantedId;
const nameMatch = !wantedId && !!wantedName && item && item.name === wantedName;
if (!idMatch && !nameMatch) return { matched: false, clip: null };
try {
return { matched: true, clip: cast(item) };
} catch (error) {
if (idMatch) throw error;
return { matched: true, clip: null };
}
}
async function probeTranscriptExport(exportTranscript) {
const json = await exportTranscript();
return typeof json === "string" && json.length > 0;
}
return {
MAX_TRANSCRIPT_JSON_BYTES,
utf8ByteLength,
parseTranscriptJSON,
searchTranscriptJSON,
transcriptRevision,
versionAtLeast,
matchingClipCandidate,
probeTranscriptExport
};
});
@@ -0,0 +1,201 @@
(function attachUniqueIdentityWorkflows(root, factory) {
const api = factory();
if (typeof module !== "undefined" && module.exports) module.exports = api;
root.PremiereMcpUniqueIdentityWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function createUniqueIdentityWorkflows() {
"use strict";
const MAX_PROJECT_ITEMS = 512;
const MAX_TOKEN_LENGTH = 512;
function createUniqueIdentityWorkflowDefinitions(deps) {
const ppro = deps && deps.ppro;
if (!ppro) throw new Error("createUniqueIdentityWorkflowDefinitions requires ppro");
const definitions = {
"object.uniqueIdentity.inspect": {
readOnly: true,
targetCapabilityProbe: true,
minHostVersion: "25.6.0",
probe: canInspectUniqueIdentity,
handler: inspectUniqueIdentity
}
};
function canInspectUniqueIdentity() {
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
ppro.UniqueSerializeable && typeof ppro.UniqueSerializeable.cast === "function");
}
async function inspectUniqueIdentity(args) {
const input = normalizeInput(args);
const firstSnapshot = await readSnapshot(ppro, input);
if (input.expectedProjectGuid && firstSnapshot.projectGuid !== input.expectedProjectGuid) {
throw createError("UXP_STALE_UNIQUE_IDENTITY", "The active project changed before unique identity inspection.");
}
if (input.expectedUniqueId && firstSnapshot.target.uniqueId !== input.expectedUniqueId) {
throw createError("UXP_STALE_UNIQUE_IDENTITY", "The requested target no longer has the expected unique identity.");
}
const secondSnapshot = await readSnapshot(ppro, input);
if (!snapshotsMatch(firstSnapshot, secondSnapshot)) {
throw createError("UXP_STALE_UNIQUE_IDENTITY", "The requested target changed during unique identity inspection.");
}
return Object.assign({}, secondSnapshot, {
verificationBoundary: "bounded_unique_serializable_double_readback"
});
}
return definitions;
}
function normalizeInput(args) {
const value = args || {};
if (!isPlainObject(value)) throw createError("UXP_INVALID_ARGUMENT", "Expected an object argument.");
const allowedKeys = ["projectItemId", "sequenceGuid", "expectedProjectGuid", "expectedUniqueId"];
Object.keys(value).forEach(function rejectUnknownKey(key) {
if (allowedKeys.indexOf(key) === -1) throw createError("UXP_INVALID_ARGUMENT", "Unsupported argument: " + key + ".");
});
const projectItemId = optionalToken(value.projectItemId, "projectItemId");
const sequenceGuid = optionalToken(value.sequenceGuid, "sequenceGuid");
if ((projectItemId ? 1 : 0) + (sequenceGuid ? 1 : 0) !== 1) {
throw createError("UXP_INVALID_ARGUMENT", "Provide exactly one of projectItemId or sequenceGuid.");
}
return {
projectItemId: projectItemId,
sequenceGuid: sequenceGuid,
expectedProjectGuid: optionalToken(value.expectedProjectGuid, "expectedProjectGuid"),
expectedUniqueId: optionalToken(value.expectedUniqueId, "expectedUniqueId")
};
}
async function readSnapshot(ppro, input) {
const project = await activeProject(ppro);
const projectGuid = await requiredGuid(project.guid, "active project GUID");
const target = input.sequenceGuid
? await sequenceTarget(ppro, project, input.sequenceGuid)
: await projectItemTarget(ppro, project, input.projectItemId);
return { projectGuid: projectGuid, target: target };
}
async function activeProject(ppro) {
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
throw createError("UXP_COMMAND_UNAVAILABLE", "Project.getActiveProject is unavailable.");
}
const project = await ppro.Project.getActiveProject();
if (!project) throw createError("UXP_NO_ACTIVE_PROJECT", "No active project is available.");
return project;
}
async function sequenceTarget(ppro, project, requestedGuid) {
if (!ppro.Guid || typeof ppro.Guid.fromString !== "function" || typeof project.getSequence !== "function") {
throw createError("UXP_COMMAND_UNAVAILABLE", "Sequence lookup is unavailable.");
}
let sequence;
try {
sequence = await project.getSequence(ppro.Guid.fromString(requestedGuid));
} catch (error) {
throw createError("UXP_TARGET_NOT_FOUND", "The requested sequence could not be resolved.");
}
if (!sequence) throw createError("UXP_TARGET_NOT_FOUND", "The requested sequence could not be resolved.");
const sequenceGuid = await requiredGuid(sequence.guid, "resolved sequence GUID");
if (sequenceGuid !== requestedGuid) {
throw createError("UXP_TARGET_NOT_FOUND", "The requested sequence could not be resolved.");
}
return {
kind: "sequence",
sequenceGuid: sequenceGuid,
uniqueId: await uniqueIdFor(ppro, sequence)
};
}
async function projectItemTarget(ppro, project, requestedProjectItemId) {
if (typeof project.getRootItem !== "function") {
throw createError("UXP_COMMAND_UNAVAILABLE", "Project.getRootItem is unavailable.");
}
const rootItem = await project.getRootItem();
if (!rootItem) throw createError("UXP_TARGET_NOT_FOUND", "The active project has no root item.");
const item = await findProjectItem(rootItem, requestedProjectItemId);
if (!item) throw createError("UXP_TARGET_NOT_FOUND", "The requested project item could not be resolved.");
return {
kind: "project_item",
projectItemId: requestedProjectItemId,
uniqueId: await uniqueIdFor(ppro, item)
};
}
async function findProjectItem(rootItem, requestedProjectItemId) {
const queue = [rootItem];
let visited = 0;
while (queue.length > 0) {
const current = queue.shift();
visited += 1;
if (visited > MAX_PROJECT_ITEMS) {
throw createError("UXP_PROJECT_TOO_LARGE", "Project item lookup exceeded the " + MAX_PROJECT_ITEMS + " item limit.");
}
if (!current || typeof current.getId !== "function") {
throw createError("UXP_COMMAND_UNAVAILABLE", "Project item ID lookup is unavailable.");
}
if (requiredToken(await current.getId(), "project item ID") === requestedProjectItemId) return current;
if (typeof current.getItems === "function") {
const children = await current.getItems();
if (!Array.isArray(children)) {
throw createError("UXP_VERIFICATION_FAILED", "Project item children were not returned as an array.");
}
Array.prototype.push.apply(queue, children);
}
}
return null;
}
async function uniqueIdFor(ppro, target) {
if (!ppro.UniqueSerializeable || typeof ppro.UniqueSerializeable.cast !== "function") {
throw createError("UXP_COMMAND_UNAVAILABLE", "UniqueSerializeable.cast is unavailable.");
}
let serializable;
try {
serializable = ppro.UniqueSerializeable.cast(target);
} catch (error) {
throw createError("UXP_COMMAND_UNAVAILABLE", "The requested target cannot be serialized uniquely.");
}
if (!serializable || typeof serializable.getUniqueID !== "function") {
throw createError("UXP_COMMAND_UNAVAILABLE", "UniqueSerializeable.getUniqueID is unavailable.");
}
return requiredGuid(await serializable.getUniqueID(), "unique identity");
}
async function requiredGuid(value, label) {
if (!value || typeof value.toString !== "function") {
throw createError("UXP_VERIFICATION_FAILED", "Expected a " + label + ".");
}
return requiredToken(await value.toString(), label);
}
function snapshotsMatch(first, second) {
if (first.projectGuid !== second.projectGuid || first.target.kind !== second.target.kind || first.target.uniqueId !== second.target.uniqueId) return false;
return first.target.kind === "sequence"
? first.target.sequenceGuid === second.target.sequenceGuid
: first.target.projectItemId === second.target.projectItemId;
}
function optionalToken(value, label) {
return value === undefined || value === null ? undefined : requiredToken(value, label);
}
function requiredToken(value, label) {
if (typeof value !== "string" || value.length === 0 || value.length > MAX_TOKEN_LENGTH) {
throw createError("UXP_INVALID_ARGUMENT", "Expected " + label + " to be a non-empty string up to " + MAX_TOKEN_LENGTH + " characters.");
}
return value;
}
function isPlainObject(value) {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function createError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
return { createUniqueIdentityWorkflowDefinitions: createUniqueIdentityWorkflowDefinitions };
});
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpWorkspace = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const CONFIG_FILE = "workspace-access.json";
function workspaceError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function parseAbsolutePath(value, label) {
if (typeof value !== "string" || !value.trim() || value.length > 4096 || value.indexOf("\0") !== -1) {
throw workspaceError("UXP_INVALID_ARGUMENT", label + " must be a non-empty absolute path of at most 4096 characters");
}
// Trimming is only valid for the blank-value check above. Leading and
// trailing spaces are legal POSIX filename characters and must survive
// normalization unchanged.
const original = value;
const windowsInput = /^[A-Za-z]:[\\/]/.test(original) || /^\\\\/.test(original);
const slashed = windowsInput ? original.replace(/\\/g, "/") : original;
const drive = /^([A-Za-z]):\/(.*)$/.exec(slashed);
const unc = windowsInput && /^\/\/([^/]+)\/([^/]+)(?:\/(.*))?$/.exec(slashed);
const posix = !drive && !unc && slashed.charAt(0) === "/";
if (!drive && !unc && !posix) {
throw workspaceError("UXP_INVALID_ARGUMENT", label + " must be absolute");
}
const prefix = drive ? drive[1].toUpperCase() + ":" : unc ? "//" + unc[1] + "/" + unc[2] : "";
const remainder = drive ? drive[2] : unc ? (unc[3] || "") : slashed.slice(1);
const parts = [];
for (const part of remainder.split("/")) {
if (!part || part === ".") continue;
if (part === "..") {
if (!parts.length) throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " escapes its filesystem root");
parts.pop();
continue;
}
if ((drive || unc) && (/[. ]$/.test(part) || part.indexOf(":") !== -1 || /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(part))) {
throw workspaceError("UXP_INVALID_ARGUMENT", label + " contains a Windows-ambiguous path segment");
}
parts.push(part);
}
const normalized = prefix + "/" + parts.join("/");
return {
normalized: normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized,
comparison: (drive || unc ? normalized.toLowerCase() : normalized),
kind: drive ? "windows" : unc ? "unc" : "posix",
depth: parts.length
};
}
function isContained(rootPath, candidatePath, allowRoot) {
const root = parseAbsolutePath(rootPath, "workspace root");
const candidate = parseAbsolutePath(candidatePath, "path");
if (root.kind !== candidate.kind) return false;
if (candidate.comparison === root.comparison) return !!allowRoot;
return candidate.comparison.indexOf(root.comparison + "/") === 0;
}
function validateLoopbackBridgeUrl(value) {
let url;
try { url = new URL(value); } catch (_) {
throw workspaceError("UXP_INVALID_BRIDGE_URL", "Bridge URL is invalid");
}
if (url.protocol !== "ws:" || (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") ||
url.pathname !== "/uxp" || url.username || url.password || url.hash) {
throw workspaceError("UXP_INVALID_BRIDGE_URL", "Bridge URL must be ws://127.0.0.1:<port>/uxp or ws://localhost:<port>/uxp");
}
url.search = "";
return url;
}
function createWorkspaceBroker(deps) {
const fs = deps && deps.fs;
const resolveCanonicalPath = deps && deps.resolveCanonicalPath;
let rootEntry = null;
let persistentToken = null;
let initialized = false;
function nativePathFor(entry) {
if (entry && typeof entry.nativePath === "string" && entry.nativePath) return entry.nativePath;
if (entry && fs && typeof fs.getNativePath === "function") return fs.getNativePath(entry);
return "";
}
async function dataFolder() {
if (!fs || typeof fs.getDataFolder !== "function") {
throw workspaceError("UXP_WORKSPACE_UNAVAILABLE", "UXP persistent storage is unavailable");
}
return fs.getDataFolder();
}
async function readConfiguration() {
try {
const folder = await dataFolder();
const file = await folder.getEntry(CONFIG_FILE);
const parsed = JSON.parse(await file.read());
if (!parsed || parsed.schemaVersion !== 1 || typeof parsed.persistentToken !== "string") return null;
return parsed;
} catch (_) {
return null;
}
}
async function writeConfiguration(token) {
const folder = await dataFolder();
const file = await folder.createFile(CONFIG_FILE, { overwrite: true });
await file.write(JSON.stringify({ schemaVersion: 1, persistentToken: token }));
}
async function deleteConfiguration() {
try {
const folder = await dataFolder();
const file = await folder.getEntry(CONFIG_FILE);
if (file && typeof file.delete === "function") await file.delete();
} catch (_) {}
}
async function initialize() {
if (initialized) return status();
initialized = true;
const stored = await readConfiguration();
if (!stored || !fs || typeof fs.getEntryForPersistentToken !== "function") return status();
try {
const entry = await fs.getEntryForPersistentToken(stored.persistentToken);
if (entry && entry.isFolder && nativePathFor(entry)) {
rootEntry = entry;
persistentToken = stored.persistentToken;
}
} catch (_) {
await deleteConfiguration();
}
return status();
}
async function requestRoot() {
if (!fs || typeof fs.getFolder !== "function" || typeof fs.createPersistentToken !== "function") {
throw workspaceError("UXP_WORKSPACE_UNAVAILABLE", "This UXP runtime cannot request persistent folder access");
}
const entry = await fs.getFolder();
const nativePath = nativePathFor(entry);
if (!entry || !entry.isFolder || !nativePath) {
throw workspaceError("UXP_WORKSPACE_NOT_SELECTED", "No workspace folder was selected");
}
if (parseAbsolutePath(nativePath, "workspace root").depth < 1) {
throw workspaceError("UXP_WORKSPACE_TOO_BROAD", "Choose a project subfolder instead of a filesystem or share root");
}
const token = await fs.createPersistentToken(entry);
if (typeof token !== "string" || !token) {
throw workspaceError("UXP_WORKSPACE_UNAVAILABLE", "Premiere did not return a persistent workspace token");
}
await writeConfiguration(token);
rootEntry = entry;
persistentToken = token;
initialized = true;
return status();
}
async function revoke() {
rootEntry = null;
persistentToken = null;
initialized = true;
await deleteConfiguration();
return status();
}
function status() {
return {
configured: !!rootEntry,
accessMode: "request",
rootName: rootEntry && typeof rootEntry.name === "string" ? rootEntry.name : null,
persistent: !!persistentToken,
pathDisclosure: "redacted",
canonicalPathValidation: typeof resolveCanonicalPath === "function" ? "available" : "unavailable"
};
}
async function assertPathAllowed(value, options) {
const label = options && options.label || "path";
const kind = options && options.kind || "file";
const rootPath = nativePathFor(rootEntry);
if (!rootEntry || !rootPath) {
throw workspaceError("UXP_WORKSPACE_REQUIRED", "Choose an approved workspace folder in the MCP for Adobe Premiere Pro panel before using " + label);
}
const candidate = parseAbsolutePath(value, label);
if (!isContained(rootPath, candidate.normalized, kind === "directory")) {
throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " must stay inside the approved workspace folder");
}
// A lexical prefix check cannot detect symlinks, Windows junctions, or
// other reparse points. Adobe's request-scoped UXP filesystem API does
// not expose a documented realpath/link-inspection primitive, so raw
// native paths must fail closed unless the embedding host supplies one.
if (typeof resolveCanonicalPath !== "function") {
throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host cannot prove that " + label + " stays inside the approved workspace after resolving filesystem links");
}
let canonicalRootValue, canonicalCandidateValue;
try {
canonicalRootValue = await resolveCanonicalPath(rootPath, { label: "workspace root", kind: "directory" });
canonicalCandidateValue = await resolveCanonicalPath(candidate.normalized, { label, kind });
} catch (error) {
if (error && /^UXP_[A-Z0-9_]+$/.test(error.code || "")) throw error;
throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
}
const canonicalRoot = parseAbsolutePath(canonicalRootValue, "workspace root");
const canonicalCandidate = parseAbsolutePath(canonicalCandidateValue, label);
if (!isContained(canonicalRoot.normalized, canonicalCandidate.normalized, kind === "directory")) {
throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " resolves outside the approved workspace folder");
}
return canonicalCandidate.normalized;
}
return { initialize, requestRoot, revoke, status, assertPathAllowed };
}
return { createWorkspaceBroker, parseAbsolutePath, isContained, validateLoopbackBridgeUrl, workspaceError };
});