premiere-pro-mcp 1.18.4 → 1.18.5

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,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.18.5] - 2026-09-28
10
+
11
+ ### Fixed
12
+
13
+ - `unnest_sequence` refuses before any change when the nest is trimmed, speed-changed or reversed, a target track is locked, or the target range already holds a clip. It now overwrites each nested clip's exact source range in place instead of inserting it, which pushed later clips down the track and ignored each clip's in and out. It reads every clip back afterwards and reports "The timeline changed … Use Undo" on any mismatch. Effects, keyframes, and transitions inside the nest are not carried over; the description says so. (#642)
14
+ - `move_clip_to_track` refuses before calling QE when either track is locked or the target range is occupied, finds the moved clip even when its node ID changes, and reports a copy left on the original track or a changed start, length, or in/out. It is now marked EXPERIMENTAL (QE) and rejects non-integer track numbers. (#642)
15
+ - `replace_clip` overwrites exactly the old clip's start and end instead of inserting the new item, which pushed later clips down the track and used the new item's full length. It verifies the span, that the old clip is gone, and that track clip counts are unchanged, and refuses on a locked track. The old clip's linked partner is left in place; the description says so. (#642)
16
+ - `export_frame_uxp` checks that the PNG exists after Premiere's exporter returns. It fails when no file was written, reports the real path when the host names the file differently (with or without the `.png` extension), and reports `committed_unverified` when UXP storage cannot check or the file already existed before the export. The panel's direct frame-export path now uses the same handler. (#642)
17
+ - `encode_media_uxp` results for jobs sent to Adobe Media Encoder carry `ameQueueStarted: "unknown"` and a note. Premiere 26.5.1 accepted UXP encodes into the Media Encoder queue without starting it. The tool does not start the queue through CEP; the note points to Media Encoder or `start_batch_encode`. (#642)
18
+ - `organize_project_items_uxp` remove verifies absence by walking the project tree instead of checking the Project panel selection first, which could still hold the removed item and turned a successful removal into `committed_unverified`. Remove now requires `project_item_id` and never falls back to the selection. (#642)
19
+ - `refresh_media` reads the interpreted frame rate before and after the refresh. When the refresh leaves an implausible rate (reported on stills as 29.97 becoming 2.75e-8), it restores the previous rate, reads it back, and reports `repaired: true`; if the restore does not stick, it fails with the manual fix. A real rate change from the source file is kept. (#642)
20
+ - `edit_timeline_uxp` MOGRT inserts (`insert_mogrt_path`, `insert_mogrt_library`) read the placement back instead of reporting the SequenceEditor return value. The result is `verified` when a returned item starts at the requested time, within one frame, on the requested video track. It fails with `UXP_VERIFICATION_FAILED` when nothing is there, and stays `committed_unverified` when the track cannot be read. Results include `sequenceId`, `requested`, and up to 16 `placements`. (#642)
21
+ - `set_sequence_pixel_aspect_ratio` compares the read-back ratio as a number, so a host that formats it as `1`, `1:1`, or `1.42222` no longer fails a correct update. A real mismatch reports the value Premiere read back. (#642)
22
+ - `play_source_monitor` fails with "No clip is loaded in the Source Monitor" instead of reporting a playback request that does nothing, names the loaded clip, and rejects a `speed` that is zero, not finite, or outside -64 to 64 before building a script. (#642)
23
+ - `create_bars_and_tone` takes an optional `bin_id`. The bin is resolved before anything is created, the new item is moved there, and its location is read back; the result is `committed_unverified` when the move cannot be confirmed. (#642)
24
+ - `get_unused_media` and `get_duplicate_media` page their output with `offset`, `limit` (1-500, default 100), and a case-insensitive `contains` name filter, and return `total`, `returned`, `truncated`, and `nextOffset`. Large projects no longer return unbounded lists. (#642)
25
+ - `get_duplicate_media` no longer groups different After Effects compositions imported from the same `.aep` or `.aepx` as duplicates. The same composition imported twice is still reported. (#642)
26
+ - `get_project_panel_metadata` caps the returned XML at `max_chars` (256-200000, default 20000) and reports `truncated` and `totalChars`. Truncated XML is marked as unsafe to pass back to `set_project_panel_metadata`. (#642)
27
+
28
+ ### Verification scope
29
+
30
+ - Automated checks validate package behavior. These changes follow live reports from Premiere 26.5.1 but have not been re-verified on a licensed Premiere host. The `unnest_sequence` and `replace_clip` overwrite path (`Track.overwriteClip` with source In/Out marks) has only been exercised against test fakes; its readback reports any host difference as a failure.
31
+
9
32
  ## [1.18.4] - 2026-09-28
10
33
 
11
34
  ### Fixed
package/README.md CHANGED
@@ -110,9 +110,9 @@ The current source exposes 384 core tools for supported workflow steps spanning
110
110
 
111
111
  <a id="latest-release"></a>
112
112
 
113
- ### Latest release: 1.18.3
113
+ ### Latest release: 1.18.4
114
114
 
115
- The published v1.18.3 npm artifact contains **384 core tools**, 382 in its default profile,
115
+ The published v1.18.4 npm artifact contains **384 core tools**, 382 in its default profile,
116
116
  and 478 with a compatible UXP connection. The development catalog above can include
117
117
  unreleased work. See the [versioned facts and package provenance](https://premiere-pro-mcp.com/facts/).
118
118
 
@@ -162,7 +162,7 @@ if the connection is unavailable.
162
162
  local Premiere processes. See the generated [supported action catalog](docs/supported-actions.md)
163
163
  for individual capability and verification contracts.
164
164
 
165
- See the [v1.18.3 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.18.3)
165
+ See the [v1.18.4 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.18.4)
166
166
  for complete details. Live installation in Premiere Pro still requires host verification.
167
167
 
168
168
  ### Current MCP protocol support
@@ -210,7 +210,7 @@ their bins, media rules, and organization rules before a facility uses one.
210
210
  > ### Install the published package (verify the name)
211
211
  >
212
212
  > ```bash
213
- > npm i -g premiere-pro-mcp@1.18.3
213
+ > npm i -g premiere-pro-mcp@1.18.4
214
214
  > ```
215
215
  >
216
216
  > 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:
@@ -218,7 +218,7 @@ their bins, media rules, and organization rules before a facility uses one.
218
218
  > | Check | Expected |
219
219
  > | --- | --- |
220
220
  > | Package name | `premiere-pro-mcp` (not `adobe-premiere-pro-mcp`) |
221
- > | Version | `1.18.3` |
221
+ > | Version | `1.18.4` |
222
222
  > | Homepage / repo | https://premiere-pro-mcp.com/ · https://github.com/leancoderkavy/premiere-pro-mcp |
223
223
  >
224
224
  > ```bash
@@ -231,9 +231,9 @@ their bins, media rules, and organization rules before a facility uses one.
231
231
 
232
232
  ### Easiest supported path: Claude Desktop
233
233
 
234
- 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.3/premiere-pro-mcp-1.18.3.mcpb).
234
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.4/premiere-pro-mcp-1.18.4.mcpb).
235
235
  2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
236
- 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.3/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.
236
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.4/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.
237
237
  4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
238
238
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
239
239
 
@@ -533,11 +533,11 @@ From a clone of this repository:
533
533
  ```bash
534
534
  codex plugin marketplace add .
535
535
  codex plugin add premiere-pro@premiere-pro-mcp
536
- npx -y premiere-pro-mcp@1.18.3 --install-cep
536
+ npx -y premiere-pro-mcp@1.18.4 --install-cep
537
537
  ```
538
538
 
539
539
  Restart Premiere Pro and start a new Codex session after installation. The plugin
540
- launches `premiere-pro-mcp@1.18.3` through `npx`; the separate CEP installation is
540
+ launches `premiere-pro-mcp@1.18.4` through `npx`; the separate CEP installation is
541
541
  required because the MCP server communicates with the running Premiere host through
542
542
  the local bridge.
543
543
 
@@ -575,7 +575,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
575
575
  Then install the Premiere bridge and start a new Claude Code session:
576
576
 
577
577
  ```bash
578
- npx -y premiere-pro-mcp@1.18.3 --install-cep
578
+ npx -y premiere-pro-mcp@1.18.4 --install-cep
579
579
  ```
580
580
 
581
581
  The Claude Code package lives in
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.4" ExtensionBundleName="MCP for Adobe After Effects">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.5" ExtensionBundleName="MCP for Adobe After Effects">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.4"/>
4
+ <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.5"/>
5
5
  </ExtensionList>
6
6
  <ExecutionEnvironment>
7
7
  <HostList>
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.18.4" ExtensionBundleName="MCP for Adobe Premiere Pro">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.18.5" ExtensionBundleName="MCP for Adobe Premiere Pro">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.4"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.4"/>
4
+ <Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.5"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.5"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -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.18.4</strong>
84
+ <strong id="updateTitle">Version 1.18.5</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.18.4";
10
+ var CURRENT_VERSION = "1.18.5";
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 =
@@ -72,6 +72,70 @@ function __writeClipSpan(item, startTicks, endTicks) {
72
72
  }
73
73
  }
74
74
 
75
+ // Tick comparisons for values that round-trip through seconds (ProjectItem
76
+ // marks take seconds). Far below one frame at any frame rate.
77
+ var __TICK_MATCH_TOL = 16;
78
+
79
+ function __isTrackLocked(track) {
80
+ try {
81
+ if (track && typeof track.isLocked === "function") return !!track.isLocked();
82
+ } catch (eLocked) {}
83
+ return false;
84
+ }
85
+
86
+ // Place exactly [inTicks, outTicks) of a project item on ONE track at
87
+ // startTicks without rippling. Track.overwriteClip places the item's In/Out
88
+ // range, so set those marks first and restore the item's own marks afterwards.
89
+ // mediaType: 1 = video, 2 = audio. Callers must prove the destination range is
90
+ // empty first; overwrite never shifts neighbours but would replace them.
91
+ // Returns { ok, attempted, marksRestored, error }. attempted=true means the
92
+ // overwrite call ran (or threw), so the timeline may have changed.
93
+ function __overwriteRangeOnTrack(track, item, startTicks, inTicks, outTicks, mediaType) {
94
+ if (!track || typeof track.overwriteClip !== "function") {
95
+ return { ok: false, attempted: false, marksRestored: true, error: "Track.overwriteClip is unavailable on this Premiere build" };
96
+ }
97
+ if (!item || typeof item.getInPoint !== "function" || typeof item.getOutPoint !== "function" ||
98
+ typeof item.setInPoint !== "function" || typeof item.setOutPoint !== "function") {
99
+ return { ok: false, attempted: false, marksRestored: true, error: "Project item In/Out marks cannot be set on this Premiere build" };
100
+ }
101
+ var originalIn = null;
102
+ var originalOut = null;
103
+ try {
104
+ originalIn = String(item.getInPoint(mediaType).ticks);
105
+ originalOut = String(item.getOutPoint(mediaType).ticks);
106
+ } catch (eMarks) {
107
+ return { ok: false, attempted: false, marksRestored: true, error: "Could not read the project item's In/Out marks" };
108
+ }
109
+ function setMarks(wantIn, wantOut) {
110
+ var currentOut = parseFloat(item.getOutPoint(mediaType).ticks);
111
+ if (parseFloat(wantIn) >= currentOut) {
112
+ item.setOutPoint(__ticksToSeconds(wantOut), mediaType);
113
+ item.setInPoint(__ticksToSeconds(wantIn), mediaType);
114
+ } else {
115
+ item.setInPoint(__ticksToSeconds(wantIn), mediaType);
116
+ item.setOutPoint(__ticksToSeconds(wantOut), mediaType);
117
+ }
118
+ return Math.abs(parseFloat(item.getInPoint(mediaType).ticks) - parseFloat(wantIn)) <= __TICK_MATCH_TOL &&
119
+ Math.abs(parseFloat(item.getOutPoint(mediaType).ticks) - parseFloat(wantOut)) <= __TICK_MATCH_TOL;
120
+ }
121
+ function restore() {
122
+ try { return setMarks(originalIn, originalOut); } catch (eRestore) { return false; }
123
+ }
124
+ var applied = false;
125
+ try { applied = setMarks(inTicks, outTicks); } catch (eSet) { applied = false; }
126
+ if (!applied) {
127
+ return { ok: false, attempted: false, marksRestored: restore(), error: "Premiere did not apply the source In/Out range to project item " + item.name };
128
+ }
129
+ var at = new Time();
130
+ at.ticks = String(startTicks);
131
+ try {
132
+ track.overwriteClip(item, at);
133
+ } catch (eOverwrite) {
134
+ return { ok: false, attempted: true, marksRestored: restore(), error: "Track.overwriteClip rejected project item " + item.name + ": " + eOverwrite.toString() };
135
+ }
136
+ return { ok: true, attempted: true, marksRestored: restore(), error: "" };
137
+ }
138
+
75
139
  function __ticksToTimecode(ticks, fps) {
76
140
  var totalSeconds = __ticksToSeconds(ticks);
77
141
  var hours = Math.floor(totalSeconds / 3600);
@@ -515,7 +515,9 @@ export function getAdvancedTools(bridgeOptions) {
515
515
  },
516
516
  },
517
517
  move_clip_to_track: {
518
- description: "Move a clip to a different track. Uses QE DOM.",
518
+ description: "Move a clip to a different track of the same type, keeping its start, duration, and source in/out. EXPERIMENTAL: uses the undocumented QE DOM moveToTrack. " +
519
+ "Refuses without changing anything when the origin or destination track is locked or the destination range is occupied. " +
520
+ "Reads the timeline back: verified only when the clip is on the destination track with the same span and source range and is gone from the origin track; committed_unverified when the source range cannot be read; otherwise failure with Undo guidance.",
519
521
  parameters: {
520
522
  type: "object",
521
523
  properties: {
@@ -531,6 +533,9 @@ export function getAdvancedTools(bridgeOptions) {
531
533
  required: ["node_id", "target_track_index"],
532
534
  },
533
535
  handler: async (args) => {
536
+ if (!Number.isSafeInteger(args.target_track_index) || args.target_track_index < 0) {
537
+ return { success: false, error: "target_track_index must be a non-negative integer" };
538
+ }
534
539
  const nodeId = escapeForExtendScript(args.node_id);
535
540
  const script = buildToolScript(`
536
541
  app.enableQE();
@@ -549,6 +554,47 @@ export function getAdvancedTools(bridgeOptions) {
549
554
  return __result({ moved: false, verified: true, alreadyOnTrack: true, clipName: result.clip.name, trackIndex: result.trackIndex });
550
555
  }
551
556
 
557
+ // Preflight: refuse before any QE call when either track is locked or
558
+ // the destination range holds another clip (QE would overwrite it).
559
+ var originTrack = targetTracks[result.trackIndex];
560
+ var destTrack = targetTracks[${args.target_track_index}];
561
+ if (__isTrackLocked(originTrack)) {
562
+ return __error("Move refused; nothing was changed. Origin " + result.trackType + " track " + result.trackIndex + " is locked.");
563
+ }
564
+ if (__isTrackLocked(destTrack)) {
565
+ return __error("Move refused; nothing was changed. Destination " + result.trackType + " track ${args.target_track_index} is locked.");
566
+ }
567
+ var moveStart = parseFloat(result.clip.start.ticks);
568
+ var moveEnd = parseFloat(result.clip.end.ticks);
569
+ for (var oi = 0; oi < destTrack.clips.numItems; oi++) {
570
+ var occupant = destTrack.clips[oi];
571
+ var occupantStart = parseFloat(occupant.start.ticks);
572
+ var occupantEnd = parseFloat(occupant.end.ticks);
573
+ if (occupantStart < moveEnd - __TICK_MATCH_TOL && occupantEnd > moveStart + __TICK_MATCH_TOL) {
574
+ return __error("Move refused; nothing was changed. The destination range " + __ticksToSeconds(moveStart) + "s-" + __ticksToSeconds(moveEnd) + "s on " + result.trackType + " track ${args.target_track_index} is already occupied by " + occupant.name + ", and moving there would overwrite it.");
575
+ }
576
+ }
577
+ var movedName = result.clip.name;
578
+ var movedSourceId = "";
579
+ try { movedSourceId = result.clip.projectItem ? String(result.clip.projectItem.nodeId) : ""; } catch (eMovedSource) {}
580
+
581
+ // Premiere can mint a new node ID when a clip changes track, so fall
582
+ // back to matching the same source item, name, and start on a track.
583
+ function findMovedOn(trackIndex) {
584
+ var track = targetTracks[trackIndex];
585
+ for (var fi = 0; fi < track.clips.numItems; fi++) {
586
+ var cand = track.clips[fi];
587
+ if (String(cand.nodeId) === "${nodeId}") return { clip: cand, trackIndex: trackIndex, trackType: result.trackType };
588
+ var candSource = "";
589
+ try { candSource = cand.projectItem ? String(cand.projectItem.nodeId) : ""; } catch (eCandSource) {}
590
+ if (movedSourceId && candSource === movedSourceId && cand.name === movedName &&
591
+ Math.abs(parseFloat(cand.start.ticks) - moveStart) <= __TICK_MATCH_TOL) {
592
+ return { clip: cand, trackIndex: trackIndex, trackType: result.trackType };
593
+ }
594
+ }
595
+ return null;
596
+ }
597
+
552
598
  var qeTrack = result.trackType === "video"
553
599
  ? qeSeq.getVideoTrackAt(result.trackIndex)
554
600
  : qeSeq.getAudioTrackAt(result.trackIndex);
@@ -586,41 +632,64 @@ export function getAdvancedTools(bridgeOptions) {
586
632
 
587
633
  // QE reports nothing useful on success, so confirm against the DOM.
588
634
  var after = __findClip("${nodeId}");
589
- if (!after) return __error("Clip ${nodeId} could not be found after the track move; the timeline may be in an unexpected state.");
635
+ if (!after) after = findMovedOn(${args.target_track_index});
636
+ if (!after) return __error("The timeline changed: clip " + movedName + " could not be found on its original or destination track after the track move. Use Undo and check the timeline.");
590
637
  if (after.trackIndex !== ${args.target_track_index}) {
638
+ if (findMovedOn(${args.target_track_index})) {
639
+ return __error("The timeline changed: Premiere placed a copy of " + movedName + " on track ${args.target_track_index} but the original is still on track " + after.trackIndex + ". Use Undo and move the clip in the Premiere UI.");
640
+ }
591
641
  return __error("Premiere accepted the moveToTrack call but the clip is still on track " + after.trackIndex + " rather than ${args.target_track_index}. Structural QE edits are known to no-op on some Premiere Pro 26.x installations (confirmed on 26.2.2).");
592
642
  }
643
+ if (findMovedOn(result.trackIndex)) {
644
+ return __error("The timeline changed: " + movedName + " now exists on both track " + result.trackIndex + " and track ${args.target_track_index}. Use Undo and move the clip in the Premiere UI.");
645
+ }
593
646
  // moveToTrack can rewrite end independently of start (#550). Re-assert
594
647
  // the original span before verifying, then fail closed if it did not hold.
595
648
  if (String(after.clip.start.ticks) !== beforeMoveStartTicks || String(after.clip.end.ticks) !== beforeMoveEndTicks) {
596
649
  try {
597
650
  __writeClipSpan(after.clip, beforeMoveStartTicks, beforeMoveEndTicks);
598
651
  } catch (spanErr) {
599
- return __error("Premiere changed the clip's timeline range during the track move and it could not be restored (" + spanErr.toString() + "). Use Undo and retry in the Premiere UI.");
652
+ return __error("The timeline changed: Premiere changed the clip's timeline range during the track move and it could not be restored (" + spanErr.toString() + "). Use Undo and retry in the Premiere UI.");
600
653
  }
601
- after = __findClip("${nodeId}");
602
- if (!after) return __error("Clip ${nodeId} could not be found after restoring its timeline range; the timeline may be in an unexpected state.");
654
+ var restored = __findClip("${nodeId}");
655
+ after = restored || findMovedOn(${args.target_track_index});
656
+ if (!after) return __error("The timeline changed: clip " + movedName + " could not be found after restoring its timeline range. Use Undo and check the timeline.");
603
657
  }
604
658
  var afterMoveStartTicks = String(after.clip.start.ticks);
605
659
  var afterMoveEndTicks = String(after.clip.end.ticks);
606
660
  if (parseFloat(afterMoveStartTicks) >= parseFloat(afterMoveEndTicks)) {
607
- return __error("Premiere left clip ${nodeId} with an inverted or empty timeline range after the track move. Use Undo and retry in the Premiere UI.");
661
+ return __error("The timeline changed: Premiere left clip " + movedName + " with an inverted or empty timeline range after the track move. Use Undo and retry in the Premiere UI.");
662
+ }
663
+ if (Math.abs(parseFloat(afterMoveStartTicks) - parseFloat(beforeMoveStartTicks)) > 1) {
664
+ return __error("The timeline changed: Premiere moved the clip to " + __ticksToSeconds(afterMoveStartTicks) + "s instead of keeping its start at " + __ticksToSeconds(beforeMoveStartTicks) + "s. Use Undo and retry in the Premiere UI.");
608
665
  }
609
666
  if (Math.abs((parseFloat(afterMoveEndTicks) - parseFloat(afterMoveStartTicks)) - spanTicks) > 1) {
610
- return __error("Premiere changed the clip duration during the track move. Use Undo and retry in the Premiere UI.");
667
+ return __error("The timeline changed: Premiere changed the clip duration during the track move. Use Undo and retry in the Premiere UI.");
611
668
  }
612
- if (String(after.clip.inPoint.ticks) !== beforeMoveInTicks || String(after.clip.outPoint.ticks) !== beforeMoveOutTicks) {
613
- return __error("Premiere changed the clip's source in/out points during the track move. Use Undo and retry in the Premiere UI.");
669
+ var sourceReadable = true;
670
+ var afterInTicks = "";
671
+ var afterOutTicks = "";
672
+ try {
673
+ afterInTicks = String(after.clip.inPoint.ticks);
674
+ afterOutTicks = String(after.clip.outPoint.ticks);
675
+ } catch (eSourceRead) {
676
+ sourceReadable = false;
677
+ }
678
+ if (sourceReadable && (afterInTicks !== beforeMoveInTicks || afterOutTicks !== beforeMoveOutTicks)) {
679
+ return __error("The timeline changed: Premiere changed the clip's source in/out points during the track move. Use Undo and retry in the Premiere UI.");
614
680
  }
615
681
 
616
- return __result({
682
+ var moveResult = {
617
683
  moved: true,
618
- verified: true,
684
+ verified: sourceReadable,
685
+ outcome: sourceReadable ? "verified" : "committed_unverified",
619
686
  clipName: after.clip.name,
620
687
  newTrackIndex: after.trackIndex,
621
688
  startSeconds: __ticksToSeconds(afterMoveStartTicks),
622
689
  endSeconds: __ticksToSeconds(afterMoveEndTicks)
623
- });
690
+ };
691
+ if (!sourceReadable) moveResult.warning = "The clip is on the destination track with the same start and duration, but its source in/out points could not be read back.";
692
+ return __result(moveResult);
624
693
  `);
625
694
  return sendCommand(script, bridgeOptions);
626
695
  },
@@ -262,7 +262,7 @@ export function getMediaTools(bridgeOptions) {
262
262
  },
263
263
  },
264
264
  refresh_media: {
265
- description: "Refresh a project item to pick up changes to the source file",
265
+ description: "Refresh a project item to pick up changes to the source file. The interpreted frame rate is read before and after; if the refresh leaves an implausible rate (seen on stills, e.g. 29.97 becoming 2.75e-8), the previous rate is restored and read back. Reports frameRateBefore, frameRateAfter and repaired.",
266
266
  parameters: {
267
267
  type: "object",
268
268
  properties: {
@@ -277,8 +277,33 @@ export function getMediaTools(bridgeOptions) {
277
277
  const script = buildToolScript(`
278
278
  var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
279
279
  if (!item) return __error("Item not found: ${escapeForExtendScript(args.item_id)}");
280
+ // Some hosts corrupt the interpreted frame rate of stills on refresh
281
+ // (#642). Snapshot it, and restore it only when the new value is not a
282
+ // usable rate, so a real change in the source file is kept.
283
+ var readRate = function () {
284
+ try {
285
+ var interp = item.getFootageInterpretation();
286
+ var rate = interp ? Number(interp.frameRate) : NaN;
287
+ return isFinite(rate) ? rate : null;
288
+ } catch (eRate) { return null; }
289
+ };
290
+ var usableRate = function (rate) { return rate !== null && rate >= 1 && rate <= 1000; };
291
+ var before = readRate();
280
292
  item.refreshMedia();
281
- return __result({ refreshed: true, item: item.name });
293
+ var after = readRate();
294
+ if (!usableRate(before) || usableRate(after)) {
295
+ return __result({ refreshed: true, item: item.name, frameRateBefore: before, frameRateAfter: after, repaired: false });
296
+ }
297
+ try {
298
+ var restore = item.getFootageInterpretation();
299
+ restore.frameRate = before;
300
+ item.setFootageInterpretation(restore);
301
+ } catch (eRestore) {}
302
+ var restored = readRate();
303
+ if (restored === null || Math.abs(restored - before) > 0.001) {
304
+ return __error("refresh_media refreshed " + item.name + ", but its frame rate changed from " + before + " to " + after + " and could not be restored (read back " + restored + "). Set it with set_footage_interpretation frame_rate " + before + ".");
305
+ }
306
+ return __result({ refreshed: true, item: item.name, frameRateBefore: before, frameRateAfter: restored, frameRateAfterRefresh: after, repaired: true });
282
307
  `);
283
308
  return sendCommand(script, bridgeOptions);
284
309
  },
@@ -35,7 +35,7 @@ export function getPlaybackTools(bridgeOptions) {
35
35
  },
36
36
  },
37
37
  play_source_monitor: {
38
- description: "Request playback of the clip in the Source Monitor. The legacy API does not provide a same-call position readback, so movement is not reported as verified.",
38
+ description: "Request playback of the clip in the Source Monitor. Fails when no clip is loaded. The legacy API does not provide a same-call position readback, so movement is not reported as verified.",
39
39
  parameters: {
40
40
  type: "object",
41
41
  properties: {
@@ -47,12 +47,23 @@ export function getPlaybackTools(bridgeOptions) {
47
47
  },
48
48
  handler: async (args) => {
49
49
  const speed = args.speed ?? 1.0;
50
+ if (typeof speed !== "number" || !Number.isFinite(speed) || speed === 0 || Math.abs(speed) > 64) {
51
+ return { success: false, error: "speed must be a non-zero number from -64 to 64." };
52
+ }
50
53
  const script = buildToolScript(`
54
+ // Premiere accepts play() with nothing loaded and does nothing (#642).
55
+ var loaded = null;
56
+ try {
57
+ if (typeof app.sourceMonitor.getProjectItem === "function") loaded = app.sourceMonitor.getProjectItem();
58
+ else loaded = undefined;
59
+ } catch (eLoaded) { loaded = undefined; }
60
+ if (loaded === null) return __error("No clip is loaded in the Source Monitor. Open one with open_in_source first.");
51
61
  app.sourceMonitor.play(${speed});
52
62
  return __result({
53
63
  playbackRequested: true,
54
64
  playbackVerified: false,
55
65
  speed: ${speed},
66
+ clip: loaded ? loaded.name : null,
56
67
  verificationScope: "Premiere accepted the source-monitor playback request only; poll get_source_monitor_position before treating movement as confirmed."
57
68
  });
58
69
  `);
@@ -298,9 +298,14 @@ export declare function getProjectTools(bridgeOptions: BridgeOptions): {
298
298
  type: string;
299
299
  description: string;
300
300
  };
301
+ bin_id: {
302
+ type: string;
303
+ description: string;
304
+ };
301
305
  };
302
306
  };
303
307
  handler: (args: {
308
+ bin_id?: string;
304
309
  width?: number;
305
310
  height?: number;
306
311
  timebase?: string;
@@ -354,8 +359,20 @@ export declare function getProjectTools(bridgeOptions: BridgeOptions): {
354
359
  };
355
360
  get_project_panel_metadata: {
356
361
  description: string;
357
- parameters: {};
358
- handler: () => Promise<import("../bridge/file-bridge.js").CommandResult>;
362
+ parameters: {
363
+ type: "object";
364
+ properties: {
365
+ max_chars: {
366
+ type: string;
367
+ minimum: number;
368
+ maximum: number;
369
+ description: string;
370
+ };
371
+ };
372
+ };
373
+ handler: (args?: {
374
+ max_chars?: number;
375
+ }) => Promise<import("../bridge/file-bridge.js").CommandResult>;
359
376
  };
360
377
  set_project_panel_metadata: {
361
378
  description: string;
@@ -1,5 +1,8 @@
1
1
  import { buildToolScript, escapeForExtendScript, } from "../bridge/script-builder.js";
2
2
  import { sendCommand } from "../bridge/file-bridge.js";
3
+ const PROJECT_PANEL_METADATA_MIN_CHARS = 256;
4
+ const PROJECT_PANEL_METADATA_MAX_CHARS = 200000;
5
+ const PROJECT_PANEL_METADATA_DEFAULT_CHARS = 20000;
3
6
  export function getProjectTools(bridgeOptions) {
4
7
  return {
5
8
  save_project: {
@@ -596,6 +599,10 @@ export function getProjectTools(bridgeOptions) {
596
599
  type: "string",
597
600
  description: "Name for the bars and tone item (default: 'Bars and Tone')",
598
601
  },
602
+ bin_id: {
603
+ type: "string",
604
+ description: "Optional destination bin: node ID, slash-separated bin path from the project root, or bin name. The bin is resolved before anything is created; the item is moved there and its location read back.",
605
+ },
599
606
  },
600
607
  },
601
608
  handler: async (args) => {
@@ -615,6 +622,9 @@ export function getProjectTools(bridgeOptions) {
615
622
  var timebase = parseFloat(${args.timebase ? `"${escapeForExtendScript(args.timebase)}"` : `seq ? seq.timebase : "254016000000"`});
616
623
  if (!isFinite(timebase) || timebase <= 0) return __error("A positive sequence timebase is required to create bars and tone.");
617
624
  var requestedName = "${escapeForExtendScript(name)}";
625
+ var targetBin = null;
626
+ ${args.bin_id !== undefined ? `targetBin = __findBin("${escapeForExtendScript(String(args.bin_id))}");
627
+ if (!targetBin) return __error("Bin not found: ${escapeForExtendScript(String(args.bin_id))}. Nothing was created.");` : ""}
618
628
  // newBarsAndTone does not reliably return a ProjectItem with name and
619
629
  // nodeId (issue #588), so diff the project tree to find what it made.
620
630
  var beforeIds = __collectNodeIds(app.project.rootItem, {});
@@ -638,12 +648,22 @@ export function getProjectTools(bridgeOptions) {
638
648
  }
639
649
  var itemName = requestedName;
640
650
  try { itemName = item.name; } catch (eName) {}
651
+ var binPlacement = null;
652
+ if (targetBin) {
653
+ try { item.moveBin(targetBin); } catch (eMove) {}
654
+ var inBin = false;
655
+ try { inBin = String(item.treePath).indexOf(String(targetBin.treePath) + "\\\\") === 0; } catch (eInBin) {}
656
+ binPlacement = { bin: targetBin.name, moved: inBin };
657
+ }
641
658
  var treePath = null;
642
659
  try { treePath = item.treePath; } catch (eTreePath) {}
660
+ var placedOk = !binPlacement || binPlacement.moved;
643
661
  return __result({
644
662
  created: true,
645
- verified: true,
646
- outcome: "verified",
663
+ verified: placedOk,
664
+ outcome: placedOk ? "verified" : "committed_unverified",
665
+ bin: binPlacement,
666
+ note: placedOk ? null : "The item was created but could not be confirmed in the requested bin; move it with move_item_to_bin.",
647
667
  name: itemName,
648
668
  nodeId: __nodeIdOf(item),
649
669
  treePath: treePath,
@@ -766,13 +786,46 @@ export function getProjectTools(bridgeOptions) {
766
786
  },
767
787
  },
768
788
  get_project_panel_metadata: {
769
- description: "Get the current Project-panel column layout as XML. This is schema/layout configuration, not per-item Scene/Shot/Take values; use inspect_project_panel_metadata_uxp item_columns or get_metadata for those.",
770
- parameters: {},
771
- handler: async () => {
789
+ description: "Get the current Project-panel column layout as XML. This is schema/layout configuration, not per-item Scene/Shot/Take values; use inspect_project_panel_metadata_uxp item_columns or get_metadata for those. Output is capped by max_chars (default 20000); when truncated is true the XML is incomplete and must not be passed to set_project_panel_metadata.",
790
+ parameters: {
791
+ type: "object",
792
+ properties: {
793
+ max_chars: {
794
+ type: "integer",
795
+ minimum: PROJECT_PANEL_METADATA_MIN_CHARS,
796
+ maximum: PROJECT_PANEL_METADATA_MAX_CHARS,
797
+ description: `Maximum characters of metadata XML to return (${PROJECT_PANEL_METADATA_MIN_CHARS}-${PROJECT_PANEL_METADATA_MAX_CHARS}, default ${PROJECT_PANEL_METADATA_DEFAULT_CHARS}). Longer XML is cut at this length and reported with truncated: true and totalChars.`,
798
+ },
799
+ },
800
+ },
801
+ handler: async (args = {}) => {
802
+ const maxChars = args?.max_chars ?? PROJECT_PANEL_METADATA_DEFAULT_CHARS;
803
+ if (typeof maxChars !== "number" ||
804
+ !Number.isInteger(maxChars) ||
805
+ maxChars < PROJECT_PANEL_METADATA_MIN_CHARS ||
806
+ maxChars > PROJECT_PANEL_METADATA_MAX_CHARS) {
807
+ return {
808
+ success: false,
809
+ error: `max_chars must be an integer between ${PROJECT_PANEL_METADATA_MIN_CHARS} and ${PROJECT_PANEL_METADATA_MAX_CHARS}`,
810
+ };
811
+ }
772
812
  const script = buildToolScript(`
773
813
  var meta = app.project.getProjectPanelMetadata();
774
814
  if (!meta) return __error("Could not retrieve project panel metadata");
775
- return __result({ metadata: meta });
815
+ var text = String(meta);
816
+ var maxChars = ${maxChars};
817
+ var totalChars = text.length;
818
+ if (totalChars > maxChars) {
819
+ return __result({
820
+ metadata: text.substring(0, maxChars),
821
+ truncated: true,
822
+ totalChars: totalChars,
823
+ returnedChars: maxChars,
824
+ maxChars: maxChars,
825
+ note: "Metadata XML was truncated and is not well-formed. Raise max_chars (up to ${PROJECT_PANEL_METADATA_MAX_CHARS}) to read more; do not pass truncated XML to set_project_panel_metadata."
826
+ });
827
+ }
828
+ return __result({ metadata: text, truncated: false, totalChars: totalChars, returnedChars: totalChars, maxChars: maxChars });
776
829
  `);
777
830
  return sendCommand(script, bridgeOptions);
778
831
  },