premiere-pro-mcp 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.22.0] - 2026-10-08
10
+
11
+ ### Fixed
12
+
13
+ - The Windows private-directory ACL check waits up to 30 seconds for `powershell.exe` instead of 5, in the server and in both CEP panels. On a loaded machine (including CI runners with coverage) PowerShell could take longer than 5 seconds to start, which made the check throw and blocked the bridge. A timeout still fails closed: the directory is not used unless the ACL check succeeds.
14
+ - Effect writes, stabilization, smart-bin creation, proxy detachment, folder imports, sequence creation, marker deletion and clip selection now distinguish requested edits from host readback, report ignored writes as failures and unreadable results as `committed_unverified`. Selection counts describe applied states; stabilization does not claim a completed analysis. CEP selected-clip removal refuses ripple requests before deleting because clip absence cannot verify gap closure.
15
+ - `set_sequence_display_format` writes Premiere's display codes (video 100-113, audio 200-201) and maps the older 0-11 / 0-1 inputs onto them. Premiere 26.5.2 stores and reads back any number, so the old values "verified" but did not select the format (2 formatted as non-drop timecode, 102 as drop-frame). `set_sequence_frame_rate` now reports the clips Premiere re-snaps to the new frame grid (`clipsMoved`, `maxShiftSeconds`, `movedClips`) with a warning that changing back does not restore them.
16
+ - `create_subclip` now returns the new subclip's `nodeId` and reads its stored range back. On Premiere 26.5.2 a soft-boundary subclip's `getInPoint`/`getOutPoint` read the whole media, so the range is read from the subclip's project metadata (`VideoInPoint`/`VideoOutPoint` timecode and frame rate). The result reports requested and observed in/out seconds, with `outcome: "verified"` when both are within one media frame of the request and `committed_unverified` otherwise or when the metadata cannot be read.
17
+ - `add_title` reports the start Premiere stored for the placed graphic. Premiere snaps it to the frame grid (a requested 5 s lands at 5.005 s at 29.97 fps), so `startSeconds` and the new `appliedStartSeconds` come from the placed clip, and the request is kept as `requestedStartSeconds`.
18
+ - `update_marker` reports the stored marker start as `timeSeconds` (for example 7.5075 s rather than the requested 7.5 s) and adds `requestedSeconds`.
19
+ - `manage_proxies` with `action: "toggle"` can set a known proxy display state. The action changes Premiere's application-wide proxy display (`app.setEnableProxies`), not the given item, and it only flipped the current state, so on Premiere 26.5.2 an agent could not ask for proxies on or off and two calls undid each other. It now accepts `enabled: true | false`, writes only when the state differs, and verifies by reading `app.getEnableProxies()` back; a host that ignores the write is reported as a failure. Omitting `enabled` still flips the state. Results report `previousEnabled`, `proxiesEnabled`, `changed` and `mode` (`set` or `flip`), and the description now says the action is application-wide.
20
+ - Still-image mark clearing now identifies unreadable still duration explicitly; `set_clip_start_time` shares the verified implementation of `set_start_time` and is documented as its alias. Colour selection states that it uses the source project item label because CEP has no timeline clip-label getter. Baked title-cache cleanup retains the newest 50 matching files and leaves unrelated directory entries untouched.
21
+
9
22
  ## [1.21.0] - 2026-10-07
10
23
 
11
24
  ### Fixed
@@ -71,6 +84,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
71
84
  - **Markers.** `manage_markers_uxp` `add` now forwards and applies `color_index` and verifies every requested field. Previously the colour was dropped while the add reported verified.
72
85
  - **Frame export.** `export_frame_uxp` retries with the file extension when Premiere rejects an extension-less name ("File Format is not supported"). It also waits up to 15 s for the PNG, which 26.5.2 writes after the call returns, and it checks for the file by walking the approved workspace folder: a plugin with request-only file access cannot open arbitrary `file:` URLs, so the old check reported a written frame as missing.
73
86
  - **Media health.** `maintain_media_health_uxp` treats a single `project_item_id` as a one-item list for inspect, refresh and set_offline.
87
+
74
88
  - `import_media` no longer reports an error when an `.xml`, `.aaf`, `.edl` or `.prproj` file imports correctly. Interchange files create sequences and bins rather than an item with the file's path, so they now return `outcome: "committed_unverified"` with `interchange: true`, which stops retries from duplicating the sequence (#805).
75
89
 
76
90
  - `duplicate_clip` no longer places a copy one frame short. Premiere can snap the source out point a frame early, so on Premiere 26.5.2 a 677-frame clip was copied as 676 frames, and the tool still reported `verified` because it allowed two frames of drift. It now trims each placed copy, and its linked partner, back to the original end, reports `endCorrected`, and verifies start, end and source in-point to within half a frame.
package/README.md CHANGED
@@ -129,13 +129,13 @@ The current source exposes 393 core tools for supported workflow steps spanning
129
129
 
130
130
  <a id="latest-release"></a>
131
131
 
132
- ### Latest release: 1.21.0
132
+ ### Latest release: 1.22.0
133
133
 
134
- The published v1.21.0 npm artifact contains **393 core tools**, 391 in its default profile,
134
+ The published v1.22.0 npm artifact contains **393 core tools**, 391 in its default profile,
135
135
  and 487 with a compatible UXP connection. The development catalog above can include
136
136
  unreleased work. See the [versioned facts and package provenance](https://premiere-pro-mcp.com/facts/).
137
137
 
138
- GitHub releases use `v<SemVer>` for both title and tag, such as `v1.21.0`.
138
+ GitHub releases use `v<SemVer>` for both title and tag, such as `v1.22.0`.
139
139
  See [release naming and version conventions](docs/release-conventions.md).
140
140
 
141
141
  ### Try a bounded workflow
@@ -154,6 +154,8 @@ if the connection is unavailable.
154
154
 
155
155
  ### Release highlights
156
156
 
157
+ - **1.22.0 readback receipts:** effect writes, stabilization, smart bins, proxies, folder imports, sequence creation, marker deletion and clip selection report what Premiere applied as `verified`, `committed_unverified` or `failed`; `create_subclip`, `add_title` and `update_marker` report the stored values; `set_sequence_frame_rate` lists clips Premiere re-snapped.
158
+ - **1.22.0 display codes and proxies:** `set_sequence_display_format` writes Premiere's real display codes (video 100-113, audio 200-201) and maps the older 0-11 and 0-1 inputs onto them; `manage_proxies` toggle can set a known proxy display state; the Windows ACL check waits up to 30 seconds for PowerShell.
157
159
  - **1.21.0 Claude Desktop startup fix:** the extension starts when optional settings are blank, and Desktop's discover-only probe no longer holds the UXP port, so the real server keeps its UXP tools.
158
160
  - **1.21.0 frame-grid verification:** project-item marks, sequence In/Out, work area, link/unlink, zero point, transitions, `replace_clip` and `duplicate_clip` verify against Premiere's own frame and sample grids and fail closed; UXP tools snap times to the sequence frame grid.
159
161
  - **1.20.0 verified range removal:** `ripple_remove_timeline_ranges` previews, then applies a multi-range ripple removal with a single-use confirmation token and clip-level readback (QE razor; experimental).
@@ -193,7 +195,7 @@ if the connection is unavailable.
193
195
  local Premiere processes. See the generated [supported action catalog](docs/supported-actions.md)
194
196
  for individual capability and verification contracts.
195
197
 
196
- See the [v1.21.0 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.21.0)
198
+ See the [v1.22.0 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.22.0)
197
199
  for complete details. Live installation in Premiere Pro still requires host verification.
198
200
 
199
201
  ### Current MCP protocol support
@@ -241,7 +243,7 @@ their bins, media rules, and organization rules before a facility uses one.
241
243
  > ### Install the published package (verify the name)
242
244
  >
243
245
  > ```bash
244
- > npm i -g premiere-pro-mcp@1.21.0
246
+ > npm i -g premiere-pro-mcp@1.22.0
245
247
  > ```
246
248
  >
247
249
  > This repository publishes only **`premiere-pro-mcp`**. A differently named package (`adobe-premiere-pro-mcp`) may also declare a `premiere-pro-mcp` executable. Before configuring a client, confirm:
@@ -249,7 +251,7 @@ their bins, media rules, and organization rules before a facility uses one.
249
251
  > | Check | Expected |
250
252
  > | --- | --- |
251
253
  > | Package name | `premiere-pro-mcp` (not `adobe-premiere-pro-mcp`) |
252
- > | Version | `1.21.0` |
254
+ > | Version | `1.22.0` |
253
255
  > | Homepage / repo | https://premiere-pro-mcp.com/ · https://github.com/leancoderkavy/premiere-pro-mcp |
254
256
  >
255
257
  > ```bash
@@ -262,9 +264,9 @@ their bins, media rules, and organization rules before a facility uses one.
262
264
 
263
265
  ### Easiest supported path: Claude Desktop
264
266
 
265
- 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.21.0/premiere-pro-mcp-1.21.0.mcpb).
267
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.22.0/premiere-pro-mcp-1.22.0.mcpb).
266
268
  2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
267
- 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.21.0/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
269
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.22.0/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
268
270
  4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
269
271
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
270
272
 
@@ -568,11 +570,11 @@ From a clone of this repository:
568
570
  ```bash
569
571
  codex plugin marketplace add .
570
572
  codex plugin add premiere-pro@premiere-pro-mcp
571
- npx -y premiere-pro-mcp@1.21.0 --install-cep
573
+ npx -y premiere-pro-mcp@1.22.0 --install-cep
572
574
  ```
573
575
 
574
576
  Restart Premiere Pro and start a new Codex session after installation. The plugin
575
- launches `premiere-pro-mcp@1.21.0` through `npx`; the separate CEP installation is
577
+ launches `premiere-pro-mcp@1.22.0` through `npx`; the separate CEP installation is
576
578
  required because the MCP server communicates with the running Premiere host through
577
579
  the local bridge.
578
580
 
@@ -610,7 +612,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
610
612
  Then install the Premiere bridge and start a new Claude Code session:
611
613
 
612
614
  ```bash
613
- npx -y premiere-pro-mcp@1.21.0 --install-cep
615
+ npx -y premiere-pro-mcp@1.22.0 --install-cep
614
616
  ```
615
617
 
616
618
  The Claude Code package lives in
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.21.0" ExtensionBundleName="MCP for Adobe After Effects">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.22.0" ExtensionBundleName="MCP for Adobe After Effects">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.21.0"/>
5
- <Extension Id="com.mcp.aftereffects.bridge.headless" Version="1.21.0"/>
4
+ <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.22.0"/>
5
+ <Extension Id="com.mcp.aftereffects.bridge.headless" Version="1.22.0"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -91,7 +91,7 @@
91
91
  encoding: "utf8",
92
92
  windowsHide: true,
93
93
  stdio: ["ignore", "pipe", "pipe"],
94
- timeout: 5000,
94
+ timeout: 30000,
95
95
  maxBuffer: 64 * 1024,
96
96
  env: commandEnvironment,
97
97
  }
Binary file
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.21.0" ExtensionBundleName="MCP for Adobe Premiere Pro">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.22.0" ExtensionBundleName="MCP for Adobe Premiere Pro">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.premiere.bridge.panel" Version="1.21.0"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.21.0"/>
4
+ <Extension Id="com.mcp.premiere.bridge.panel" Version="1.22.0"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.22.0"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -91,7 +91,7 @@
91
91
  encoding: "utf8",
92
92
  windowsHide: true,
93
93
  stdio: ["ignore", "pipe", "pipe"],
94
- timeout: 5000,
94
+ timeout: 30000,
95
95
  maxBuffer: 64 * 1024,
96
96
  env: commandEnvironment,
97
97
  }
@@ -81,7 +81,7 @@
81
81
  <section class="update-section" aria-live="polite">
82
82
  <div class="update-copy">
83
83
  <span class="section-label">MCP updates</span>
84
- <strong id="updateTitle">Version 1.21.0</strong>
84
+ <strong id="updateTitle">Version 1.22.0</strong>
85
85
  <span id="updateDetail">Checking the global MCP server and connector release…</span>
86
86
  </div>
87
87
  <button id="btnUpdate" class="button button-update" onclick="handleUpdateClick()" type="button" aria-describedby="updateDetail" disabled>
@@ -7,7 +7,7 @@
7
7
  })(this, function () {
8
8
  "use strict";
9
9
 
10
- var CURRENT_VERSION = "1.21.0";
10
+ var CURRENT_VERSION = "1.22.0";
11
11
  var PACKAGE_NAME = "premiere-pro-mcp";
12
12
  var LATEST_PACKAGE_API = "https://registry.npmjs.org/" + PACKAGE_NAME;
13
13
  var LATEST_RELEASE_API =
@@ -183,6 +183,9 @@ export const WINDOWS_BRIDGE_ACL_SCRIPT = [
183
183
  '})',
184
184
  '[pscustomobject]@{ ownerSid = $owner; currentUserSid = $current; unsafeWriteAces = $unsafe; unsafeAncestorEntries = $unsafeAncestors } | ConvertTo-Json -Compress',
185
185
  ].join("\n");
186
+ // Starting powershell.exe can take several seconds on a loaded Windows machine (CI runners
187
+ // with coverage timed out at 5 s). A timeout still fails closed: the caller throws.
188
+ const WINDOWS_ACL_CHECK_TIMEOUT_MS = 30_000;
186
189
  export function inspectWindowsBridgeDirectoryAcl(directory, initialize, privacy = {}) {
187
190
  let script = WINDOWS_BRIDGE_ACL_SCRIPT;
188
191
  if (privacy.readAccess) {
@@ -198,7 +201,7 @@ export function inspectWindowsBridgeDirectoryAcl(directory, initialize, privacy
198
201
  encoding: "utf8",
199
202
  windowsHide: true,
200
203
  stdio: ["ignore", "pipe", "pipe"],
201
- timeout: 5000,
204
+ timeout: WINDOWS_ACL_CHECK_TIMEOUT_MS,
202
205
  maxBuffer: 64 * 1024,
203
206
  env: {
204
207
  ...process.env,
@@ -888,7 +888,7 @@ export function getAdvancedTools(bridgeOptions, dependencies = {}) {
888
888
  },
889
889
  },
890
890
  set_clip_selection: {
891
- description: "Select or deselect a clip in the active sequence",
891
+ description: "Select or deselect a clip in the active sequence and read its selection state back.",
892
892
  parameters: {
893
893
  type: "object",
894
894
  properties: {
@@ -904,12 +904,18 @@ export function getAdvancedTools(bridgeOptions, dependencies = {}) {
904
904
  required: ["node_id", "selected"],
905
905
  },
906
906
  handler: async (args) => {
907
+ if (typeof args.selected !== "boolean")
908
+ return { success: false, error: "selected must be a boolean" };
907
909
  const script = buildToolScript(`
908
910
  var result = __findClip("${escapeForExtendScript(args.node_id)}");
909
911
  if (!result) return __error("Clip not found");
910
-
911
912
  result.clip.setSelected(${args.selected ? 1 : 0}, true);
912
- return __result({ selected: ${args.selected}, clipName: result.clip.name });
913
+ var applied = null;
914
+ try { var observed = result.clip.isSelected(); if (typeof observed === "boolean" || observed === 0 || observed === 1) applied = !!observed; } catch (readError) {}
915
+ var verified = applied === ${args.selected};
916
+ var receipt = { selected: applied, requestedSelected: ${args.selected}, clipName: result.clip.name, verified: verified, outcome: applied === null ? "committed_unverified" : (verified ? "verified" : "failed") };
917
+ if (applied !== null && !verified) return __jsonStringify({ success: false, error: "Premiere did not apply the requested clip selection.", data: receipt });
918
+ return __result(receipt);
913
919
  `);
914
920
  return sendCommand(script, bridgeOptions);
915
921
  },
@@ -1087,7 +1093,7 @@ export function getAdvancedTools(bridgeOptions, dependencies = {}) {
1087
1093
  },
1088
1094
  },
1089
1095
  create_sequence_from_clips: {
1090
- description: "Create a new sequence by automatically placing project items in order",
1096
+ description: "Create a new sequence from project items, then verify its registered identity, name and source-item placements. clipCount reports observed track items; requestedClipCount reports requested source items.",
1091
1097
  parameters: {
1092
1098
  type: "object",
1093
1099
  properties: {
@@ -1104,6 +1110,8 @@ export function getAdvancedTools(bridgeOptions, dependencies = {}) {
1104
1110
  required: ["name", "item_ids"],
1105
1111
  },
1106
1112
  handler: async (args) => {
1113
+ if (!args.name || !Array.isArray(args.item_ids) || !args.item_ids.length || args.item_ids.some((id) => typeof id !== "string" || !id))
1114
+ return { success: false, error: "name and at least one non-empty item_id are required" };
1107
1115
  const itemLookups = args.item_ids
1108
1116
  .map((id, i) => `var item${i} = __findProjectItem("${escapeForExtendScript(id)}"); if (!item${i}) return __error("Item not found: ${escapeForExtendScript(id)}"); items.push(item${i});`)
1109
1117
  .join("\n ");
@@ -1111,9 +1119,48 @@ export function getAdvancedTools(bridgeOptions, dependencies = {}) {
1111
1119
  var items = [];
1112
1120
  ${itemLookups}
1113
1121
 
1122
+ var beforeIds = {};
1123
+ for (var b = 0; b < app.project.sequences.numSequences; b++) beforeIds[String(app.project.sequences[b].sequenceID)] = true;
1114
1124
  var seq = app.project.createNewSequenceFromClips("${escapeForExtendScript(args.name)}", items);
1115
1125
  if (!seq) return __error("Failed to create sequence from clips");
1116
- return __result({ created: true, name: seq.name, id: seq.sequenceID, clipCount: items.length });
1126
+ var registered = null;
1127
+ for (var r = 0; r < app.project.sequences.numSequences; r++) if (String(app.project.sequences[r].sequenceID) === String(seq.sequenceID)) registered = app.project.sequences[r];
1128
+ var observedItems = [], appliedCount = 0, readable = true;
1129
+ try {
1130
+ if (!registered || !String(seq.sequenceID)) readable = false;
1131
+ else {
1132
+ var groups = [registered.videoTracks, registered.audioTracks];
1133
+ for (var g = 0; g < groups.length; g++) {
1134
+ var tracks = groups[g];
1135
+ if (!tracks || typeof tracks.numTracks !== "number") { readable = false; continue; }
1136
+ for (var t = 0; t < tracks.numTracks; t++) {
1137
+ for (var c = 0; c < tracks[t].clips.numItems; c++) {
1138
+ var clip = tracks[t].clips[c];
1139
+ appliedCount++;
1140
+ if (!clip.projectItem || !clip.projectItem.nodeId || !clip.start || !isFinite(Number(clip.start.ticks))) { readable = false; continue; }
1141
+ observedItems.push({ id: String(clip.projectItem.nodeId), start: Number(clip.start.ticks), kind: g });
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ } catch (readError) { readable = false; }
1147
+ var placementVerified = readable;
1148
+ var lastStart = -1;
1149
+ var used = {};
1150
+ for (var i = 0; i < items.length && readable; i++) {
1151
+ var match = -1;
1152
+ for (var k = 0; k < observedItems.length; k++) {
1153
+ if (!used[k] && observedItems[k].id === String(items[i].nodeId) && observedItems[k].start >= lastStart && (match < 0 || observedItems[k].start < observedItems[match].start)) match = k;
1154
+ }
1155
+ if (match < 0) { placementVerified = false; break; }
1156
+ lastStart = observedItems[match].start;
1157
+ for (var u = 0; u < observedItems.length; u++) if (observedItems[u].id === observedItems[match].id && observedItems[u].start === lastStart) used[u] = true;
1158
+ }
1159
+ var identityVerified = !!registered && !!String(seq.sequenceID) && !beforeIds[String(seq.sequenceID)] && registered.name === "${escapeForExtendScript(args.name)}";
1160
+ var verified = identityVerified && placementVerified;
1161
+ var receipt = { created: !!registered, name: registered ? registered.name : null, id: seq.sequenceID, clipCount: readable ? appliedCount : null, requestedClipCount: items.length, verified: verified, outcome: !readable ? "committed_unverified" : (verified ? "verified" : "failed"), verification: "Sequence identity, name and ordered source placements only; playback and persistence remain unverified." };
1162
+ if (readable && !verified) return __jsonStringify({ success: false, error: "Created sequence did not match the requested identity, name or source placements.", data: receipt });
1163
+ return __result(receipt);
1117
1164
  `);
1118
1165
  return sendCommand(script, bridgeOptions);
1119
1166
  },
@@ -587,7 +587,7 @@ export function getEffectsTools(bridgeOptions) {
587
587
  },
588
588
  },
589
589
  stabilize_clip: {
590
- description: "Apply the Warp Stabilizer effect to a clip for video stabilization. Uses QE DOM.",
590
+ description: "Apply Warp Stabilizer using the experimental QE DOM. Verifies component addition and readable parameter values, never analysis completion. Numeric or unreadable Method popup values remain unverified because no documented label mapping is available.",
591
591
  parameters: {
592
592
  type: "object",
593
593
  properties: {
@@ -608,6 +608,10 @@ export function getEffectsTools(bridgeOptions) {
608
608
  required: ["node_id"],
609
609
  },
610
610
  handler: async (args) => {
611
+ if (args.smoothness !== undefined && (!Number.isFinite(args.smoothness) || args.smoothness < 0 || args.smoothness > 100))
612
+ return { success: false, error: "smoothness must be a finite percentage from 0 to 100" };
613
+ if (args.method !== undefined && !["Subspace Warp", "Position", "Position, Scale, Rotation"].includes(args.method))
614
+ return { success: false, error: "Unsupported stabilization method" };
611
615
  const script = buildToolScript(`
612
616
  app.enableQE();
613
617
  var qeSeq = qe.project.getActiveSequence();
@@ -625,6 +629,8 @@ export function getEffectsTools(bridgeOptions) {
625
629
  var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
626
630
  if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
627
631
 
632
+ var componentCountBefore = result.clip.components.numItems;
633
+ if (typeof componentCountBefore !== "number" || !isFinite(componentCountBefore)) return __error("Cannot read clip components; nothing was changed.");
628
634
  // Find and apply Warp Stabilizer
629
635
  var effectCatalog = __getQeEffectCatalog("video");
630
636
  if (!effectCatalog.ok) return __error(effectCatalog.error);
@@ -640,32 +646,59 @@ export function getEffectsTools(bridgeOptions) {
640
646
 
641
647
  if (!found) return __error("Warp Stabilizer effect not found");
642
648
 
643
- // Set properties if specified
644
649
  var clip = result.clip;
645
- var changes = { stabilized: true };
646
- ${args.smoothness !== undefined || args.method !== undefined ? `
647
- for (var i = 0; i < clip.components.numItems; i++) {
648
- var comp = clip.components[i];
649
- if (comp.displayName === "Warp Stabilizer") {
650
- for (var p = 0; p < comp.properties.numItems; p++) {
651
- var prop = comp.properties[p];
652
- ${args.smoothness !== undefined ? `
653
- if (prop.displayName === "Smoothness") {
654
- prop.setValue(${args.smoothness}, true);
655
- changes.smoothness = ${args.smoothness};
656
- }` : ""}
657
- ${args.method !== undefined ? `
658
- if (prop.displayName === "Method") {
659
- prop.setValue("${escapeForExtendScript(args.method)}", true);
660
- changes.method = "${escapeForExtendScript(args.method)}";
661
- }` : ""}
650
+ var afterCount = null;
651
+ var stabilizer = null;
652
+ try {
653
+ afterCount = clip.components.numItems;
654
+ if (typeof afterCount !== "number" || !isFinite(afterCount)) afterCount = null;
655
+ if (afterCount !== null) {
656
+ for (var componentIndex = componentCountBefore; componentIndex < afterCount; componentIndex++) {
657
+ if (clip.components[componentIndex].displayName === "Warp Stabilizer") stabilizer = clip.components[componentIndex];
662
658
  }
663
- break;
664
659
  }
660
+ } catch (componentReadError) { afterCount = null; }
661
+ var changes = { stabilized: false };
662
+ var data = { clipName: clip.name, changes: changes, requested: { smoothness: ${args.smoothness === undefined ? "null" : args.smoothness}, method: ${args.method === undefined ? "null" : `"${escapeForExtendScript(args.method)}"`} }, applied: { smoothness: null, method: null }, componentCountBefore: componentCountBefore, componentCountAfter: afterCount, verified: false, outcome: "committed_unverified", analysisStatus: "unknown", info: "Effect and parameter readback only; analysis completion and rendered stabilization require separate inspection." };
663
+ if (afterCount !== null && afterCount <= componentCountBefore) { data.outcome = "failed"; return __error("Premiere did not add Warp Stabilizer.", data); }
664
+ if (!stabilizer) return __result(data);
665
+ changes.stabilized = true;
666
+ var parameterVerified = true;
667
+ var parameterMismatch = false;
668
+ var warnings = [];
669
+ function setStabilizerParameter(name, requested, field) {
670
+ if (requested === null) return;
671
+ var property = null;
672
+ var matches = 0;
673
+ for (var propertyIndex = 0; propertyIndex < stabilizer.properties.numItems; propertyIndex++) {
674
+ if (stabilizer.properties[propertyIndex].displayName === name) { property = stabilizer.properties[propertyIndex]; matches++; }
675
+ }
676
+ if (matches !== 1) { parameterVerified = false; warnings.push(name + " property is unavailable or ambiguous."); return; }
677
+ try {
678
+ if (name === "Method") {
679
+ var currentMethod = property.getValue();
680
+ if (typeof currentMethod !== "string" || (currentMethod !== "Subspace Warp" && currentMethod !== "Position" && currentMethod !== "Position, Scale, Rotation")) {
681
+ parameterVerified = false;
682
+ warnings.push("Method popup mapping is unavailable; method was not changed.");
683
+ return;
684
+ }
685
+ }
686
+ property.setValue(requested, true);
687
+ var actual = property.getValue();
688
+ if (actual === undefined || actual === null || (typeof actual === "number" && !isFinite(actual))) { parameterVerified = false; return; }
689
+ data.applied[field] = actual;
690
+ changes[field] = actual;
691
+ var same = typeof actual === "number" && typeof requested === "number" ? Math.abs(actual - requested) <= 0.0001 : actual === requested;
692
+ if (!same) { parameterMismatch = true; parameterVerified = false; }
693
+ } catch (parameterError) { parameterVerified = false; warnings.push(name + " write or readback unavailable: " + String(parameterError)); }
665
694
  }
666
- ` : ""}
667
-
668
- return __result({ clipName: clip.name, info: "Warp Stabilizer applied. Analysis will begin automatically.", changes: changes });
695
+ setStabilizerParameter("Smoothness", data.requested.smoothness, "smoothness");
696
+ setStabilizerParameter("Method", data.requested.method, "method");
697
+ data.warnings = warnings;
698
+ data.verified = parameterVerified;
699
+ data.outcome = parameterMismatch ? "failed" : (parameterVerified ? "verified" : "committed_unverified");
700
+ if (parameterMismatch) return __error("Warp Stabilizer parameter readback does not match the requested value.", data);
701
+ return __result(data);
669
702
  `);
670
703
  return sendCommand(script, bridgeOptions);
671
704
  },
@@ -862,6 +862,10 @@ export declare function getExportTools(bridgeOptions: BridgeOptions): {
862
862
  type: string;
863
863
  description: string;
864
864
  };
865
+ enabled: {
866
+ type: string;
867
+ description: string;
868
+ };
865
869
  };
866
870
  required: string[];
867
871
  };
@@ -872,6 +876,7 @@ export declare function getExportTools(bridgeOptions: BridgeOptions): {
872
876
  output_path?: string;
873
877
  preset_path?: string;
874
878
  start_batch?: boolean;
879
+ enabled?: boolean;
875
880
  }) => Promise<import("../bridge/file-bridge.js").CommandResult | {
876
881
  success: boolean;
877
882
  error: string;
@@ -1549,7 +1549,7 @@ export function getExportTools(bridgeOptions) {
1549
1549
  },
1550
1550
  },
1551
1551
  create_subclip: {
1552
- description: "Create a subclip from a project item with in/out points",
1552
+ description: "Create a subclip from a project item with in/out points. Returns the new item's nodeId and reads its stored range back from Premiere project metadata: outcome verified when the observed in/out seconds are within one media frame of the request, otherwise committed_unverified.",
1553
1553
  parameters: {
1554
1554
  type: "object",
1555
1555
  properties: {
@@ -1573,13 +1573,18 @@ export function getExportTools(bridgeOptions) {
1573
1573
  required: ["item_id", "name", "in_seconds", "out_seconds"],
1574
1574
  },
1575
1575
  handler: async (args) => {
1576
+ if (!Number.isFinite(args.in_seconds) || !Number.isFinite(args.out_seconds) || args.in_seconds < 0 || args.out_seconds <= args.in_seconds) {
1577
+ return { success: false, error: "in_seconds and out_seconds must be finite, in_seconds must be non-negative, and out_seconds must be after in_seconds." };
1578
+ }
1576
1579
  const script = buildToolScript(`
1577
1580
  var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
1578
1581
  if (!item) return __error("Item not found");
1579
-
1580
- var inTicks = __secondsToTicks(${args.in_seconds}).toString();
1581
- var outTicks = __secondsToTicks(${args.out_seconds}).toString();
1582
-
1582
+
1583
+ var requestedInSeconds = ${args.in_seconds};
1584
+ var requestedOutSeconds = ${args.out_seconds};
1585
+ var inTicks = __secondsToTicks(requestedInSeconds).toString();
1586
+ var outTicks = __secondsToTicks(requestedOutSeconds).toString();
1587
+
1583
1588
  var subclip = item.createSubClip(
1584
1589
  "${escapeForExtendScript(args.name)}",
1585
1590
  inTicks,
@@ -1588,9 +1593,80 @@ export function getExportTools(bridgeOptions) {
1588
1593
  1, // takeVideo
1589
1594
  1 // takeAudio
1590
1595
  );
1591
-
1596
+
1592
1597
  if (!subclip) return __error("Failed to create subclip");
1593
- return __result({ created: true, name: "${escapeForExtendScript(args.name)}", source: item.name });
1598
+
1599
+ // A soft-boundary subclip's getInPoint/getOutPoint read the whole media
1600
+ // on Premiere 26.5, so read the stored range from project metadata.
1601
+ // Each point holds an rdf:value timecode and a frame_rate child in
1602
+ // ticks per frame; the Out timecode names the last frame (inclusive).
1603
+ function __subclipTimecodeFrames(timecode, frameTicks) {
1604
+ var parts = String(timecode).match(/^\\s*(\\d+)[:;.](\\d+)[:;.](\\d+)([:;.])(\\d+)\\s*$/);
1605
+ if (!parts) return NaN;
1606
+ var nominal = Math.round(TICKS_PER_SECOND / frameTicks);
1607
+ var hours = parseInt(parts[1], 10);
1608
+ var minutes = parseInt(parts[2], 10);
1609
+ var frames = ((hours * 60 + minutes) * 60 + parseInt(parts[3], 10)) * nominal + parseInt(parts[5], 10);
1610
+ if (parts[4] === ";" && (nominal === 30 || nominal === 60)) {
1611
+ var totalMinutes = hours * 60 + minutes;
1612
+ frames -= (nominal / 15) * (totalMinutes - Math.floor(totalMinutes / 10));
1613
+ }
1614
+ return frames;
1615
+ }
1616
+ function __subclipPoint(xml, column) {
1617
+ var tagName = "Column.Intrinsic." + column;
1618
+ var openStart = xml.indexOf(tagName);
1619
+ if (openStart < 0) return null;
1620
+ var openEnd = xml.indexOf(">", openStart);
1621
+ if (openEnd < 0 || xml.charAt(openEnd - 1) === "/") return null;
1622
+ var closeStart = xml.indexOf(tagName, openEnd);
1623
+ if (closeStart < 0) return null;
1624
+ var body = xml.substring(openEnd + 1, closeStart);
1625
+ var value = body.match(/<rdf:value>\\s*([^<]*?)\\s*<\\/rdf:value>/);
1626
+ var rate = body.match(/frame_rate>\\s*(\\d+)\\s*</);
1627
+ if (!value || !rate) return null;
1628
+ var frameTicks = parseFloat(rate[1]);
1629
+ if (!isFinite(frameTicks) || frameTicks <= 0) return null;
1630
+ var frames = __subclipTimecodeFrames(value[1], frameTicks);
1631
+ if (!isFinite(frames)) return null;
1632
+ return { frames: frames, frameTicks: frameTicks, timecode: value[1] };
1633
+ }
1634
+
1635
+ var nodeId = null;
1636
+ try { nodeId = subclip.nodeId ? String(subclip.nodeId) : null; } catch (nodeIdError) {}
1637
+ var receipt = {
1638
+ created: true,
1639
+ nodeId: nodeId,
1640
+ name: "${escapeForExtendScript(args.name)}",
1641
+ source: item.name,
1642
+ requestedInSeconds: requestedInSeconds,
1643
+ requestedOutSeconds: requestedOutSeconds,
1644
+ observedInSeconds: null,
1645
+ observedOutSeconds: null,
1646
+ outcome: "committed_unverified",
1647
+ verified: false
1648
+ };
1649
+ try { if (subclip.name) receipt.name = String(subclip.name); } catch (nameError) {}
1650
+ var metadata = null;
1651
+ try { metadata = subclip.getProjectMetadata ? String(subclip.getProjectMetadata()) : null; } catch (metadataError) {}
1652
+ var inPoint = metadata ? __subclipPoint(metadata, "VideoInPoint") : null;
1653
+ var outPoint = metadata ? __subclipPoint(metadata, "VideoOutPoint") : null;
1654
+ if (!inPoint || !outPoint) {
1655
+ receipt.note = "Premiere created the subclip, but its stored in/out range could not be read from project metadata.";
1656
+ return __result(receipt);
1657
+ }
1658
+ receipt.observedInSeconds = __ticksToSeconds(inPoint.frames * inPoint.frameTicks);
1659
+ receipt.observedOutSeconds = __ticksToSeconds((outPoint.frames + 1) * outPoint.frameTicks);
1660
+ receipt.observedInTimecode = inPoint.timecode;
1661
+ receipt.observedOutTimecode = outPoint.timecode;
1662
+ var tolerance = __ticksToSeconds(Math.max(inPoint.frameTicks, outPoint.frameTicks)) + 0.000001;
1663
+ if (Math.abs(receipt.observedInSeconds - requestedInSeconds) <= tolerance && Math.abs(receipt.observedOutSeconds - requestedOutSeconds) <= tolerance) {
1664
+ receipt.outcome = "verified";
1665
+ receipt.verified = true;
1666
+ } else {
1667
+ receipt.note = "Premiere stored a subclip range more than one media frame from the request.";
1668
+ }
1669
+ return __result(receipt);
1594
1670
  `);
1595
1671
  return sendCommand(script, bridgeOptions);
1596
1672
  },
@@ -1910,7 +1986,10 @@ export function getExportTools(bridgeOptions) {
1910
1986
  },
1911
1987
  },
1912
1988
  manage_proxies: {
1913
- description: "Create, attach, or toggle proxies for a project item. " +
1989
+ description: "Create or attach proxies for a project item, or set Premiere's proxy display. " +
1990
+ "'toggle' is application-wide, not per item: it changes app.getEnableProxies()/setEnableProxies() for every sequence and monitor, " +
1991
+ "and item_id is only checked to exist. Pass enabled: true or false to set a known state; omitting enabled flips the current state, " +
1992
+ "so two calls undo each other. The result reports previousEnabled and proxiesEnabled read back from Premiere. " +
1914
1993
  "Note: 'create' only requests a proxy encode from Adobe Media Encoder and returns an unverified handoff. " +
1915
1994
  "Independently verify the AME queue or output file before calling this tool again with action 'attach' " +
1916
1995
  "and proxy_path set to the output_path you passed here. Optional start_batch requests processing of every ready AME queue job, including unrelated jobs. " +
@@ -1946,10 +2025,21 @@ export function getExportTools(bridgeOptions) {
1946
2025
  type: "boolean",
1947
2026
  description: AME_START_BATCH_DESCRIPTION,
1948
2027
  },
2028
+ enabled: {
2029
+ type: "boolean",
2030
+ description: "For 'toggle' only: sets the application-wide proxy display to this state (true shows proxies, false shows full-resolution media) " +
2031
+ "and verifies it by reading app.getEnableProxies() back. Omit to flip the current state, which is discouraged because the outcome depends on a state you have not read.",
2032
+ },
1949
2033
  },
1950
2034
  required: ["item_id", "action"],
1951
2035
  },
1952
2036
  handler: async (args) => {
2037
+ if (!["create", "attach", "toggle"].includes(args.action)) {
2038
+ return { success: false, error: "action must be create, attach, or toggle. Nothing was changed." };
2039
+ }
2040
+ if (args.enabled !== undefined && typeof args.enabled !== "boolean") {
2041
+ return { success: false, error: "enabled must be a boolean" };
2042
+ }
1953
2043
  let presetPath = args.preset_path;
1954
2044
  const skippedPresets = [];
1955
2045
  if (args.action === "create" && args.output_path && !presetPath) {
@@ -2035,11 +2125,27 @@ export function getExportTools(bridgeOptions) {
2035
2125
  if (typeof app.getEnableProxies !== "function" || typeof app.setEnableProxies !== "function") {
2036
2126
  return __error("This Premiere build does not expose app.getEnableProxies/setEnableProxies; toggle proxies in the Program Monitor.");
2037
2127
  }
2128
+ var requestedEnabled = ${args.enabled === undefined ? "null" : args.enabled ? "true" : "false"};
2038
2129
  var wasEnabled = Number(app.getEnableProxies()) === 1;
2039
- app.setEnableProxies(wasEnabled ? 0 : 1);
2130
+ var targetEnabled = requestedEnabled === null ? !wasEnabled : requestedEnabled;
2131
+ if (targetEnabled !== wasEnabled) app.setEnableProxies(targetEnabled ? 1 : 0);
2040
2132
  var nowEnabled = Number(app.getEnableProxies()) === 1;
2041
- if (nowEnabled === wasEnabled) return __error("Premiere did not change the proxy display setting.");
2042
- return __result({ action: "toggle", proxiesEnabled: nowEnabled, verified: true, scope: "application-wide proxy display" });
2133
+ if (nowEnabled !== targetEnabled) {
2134
+ return __error(
2135
+ "Premiere did not apply the proxy display setting: requested " + (targetEnabled ? "enabled" : "disabled") + ", read back " + (nowEnabled ? "enabled" : "disabled") + ".",
2136
+ { action: "toggle", previousEnabled: wasEnabled, requestedEnabled: targetEnabled, proxiesEnabled: nowEnabled, verified: false, outcome: "failed", scope: "application-wide proxy display" }
2137
+ );
2138
+ }
2139
+ return __result({
2140
+ action: "toggle",
2141
+ mode: requestedEnabled === null ? "flip" : "set",
2142
+ previousEnabled: wasEnabled,
2143
+ proxiesEnabled: nowEnabled,
2144
+ changed: nowEnabled !== wasEnabled,
2145
+ verified: true,
2146
+ outcome: "verified",
2147
+ scope: "application-wide proxy display"
2148
+ });
2043
2149
  }
2044
2150
 
2045
2151
  return __error("Unknown proxy action: " + action);