premiere-pro-mcp 1.18.2 → 1.18.4
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 +30 -0
- package/README.md +11 -11
- package/after-effects-cep-plugin/CSXS/manifest.xml +2 -2
- package/artifacts/MCPBridgeCEP.zxp +0 -0
- package/cep-plugin/CSXS/manifest.xml +3 -3
- package/cep-plugin/index.html +1 -1
- package/cep-plugin/updater.cjs +1 -1
- package/dist/bridge/script-builder.js +8 -0
- package/dist/bridge/uxp-websocket-bridge.d.ts +2 -0
- package/dist/bridge/uxp-websocket-bridge.js +26 -7
- package/dist/security/path-guard.d.ts +10 -0
- package/dist/security/path-guard.js +106 -0
- package/dist/tools/advanced.js +20 -12
- package/dist/tools/clipboard.js +75 -15
- package/dist/tools/effects.js +23 -15
- package/dist/tools/export.js +6 -1
- package/dist/tools/playhead.js +19 -6
- package/dist/tools/track-targeting.js +22 -3
- package/dist/tools/uxp.js +1 -1
- package/docs/supported-actions.md +1 -1
- package/package.json +1 -1
- package/public-product-manifest.json +1 -1
- package/uxp-plugin/manifest.json +1 -1
- package/uxp-plugin/next-workflows.cjs +45 -7
- package/uxp-plugin/transcript-import.cjs +6 -0
- package/uxp-plugin/workflows.cjs +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ 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.4] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `import_transcript_uxp` refuses to import over an existing transcript (`UXP_TRANSCRIPT_OVERWRITE_REFUSED`). On Premiere 26.5.1 a failed import cleared an existing transcript that could not be restored. (#642)
|
|
14
|
+
- UXP commands larger than the panel's 64 KiB frame limit are refused by the server with `UXP_COMMAND_TOO_LARGE` instead of timing out after 30 seconds. (#642)
|
|
15
|
+
- `manage_metadata_uxp` update and field update fail with `UXP_METADATA_NOT_APPLIED` when Premiere commits the transaction but the metadata reads back unchanged, instead of reporting `updated: true`. (#642)
|
|
16
|
+
- UXP project Save As, create, and branch copies ask for `confirmOverwrite` only when the destination file exists. Premiere 26.5.1's `Project.isProject` reports true for any `.prproj` path. (#642)
|
|
17
|
+
- CEP tools that act on a timeline clip through the QE DOM no longer pass the DOM clip index to `qeTrack.getItemAt()`. QE counts gaps as track items, so on a track with a leading or intermediate gap these tools renamed, re-effected, or retimed the wrong clip. `rename_clip`, `remove_all_effects`, `set_frame_blend`, `set_time_interpolation`, `apply_effect`, `apply_audio_effect`, `color_correct`, `apply_lut`, `stabilize_clip`, `copy_effects_between_clips`, `batch_rename_clips`, and `get_qe_clip_info` now match the QE clip by timeline start and fail before any change when no clip matches. `copy_effects_between_clips` reports each effect as verified, committed_unverified, or failed from a readback of the target's components instead of silently dropping errors. `apply_effect` and `apply_audio_effect` also no longer emit a malformed error string that stopped the generated script from parsing. (#642)
|
|
18
|
+
|
|
19
|
+
### Verification scope
|
|
20
|
+
|
|
21
|
+
- 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 QE clip lookup change has not been verified on a licensed Premiere host either.
|
|
22
|
+
|
|
23
|
+
## [1.18.3] - 2026-09-28
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- UXP path arguments are now checked in the MCP server process before a command is sent. Any absolute path that goes through a symbolic link or directory junction is refused with `UXP_PATH_SYMLINK_REFUSED`. Premiere 26.5 UXP reports a link's own path and cannot `lstat`, so the panel's workspace check could be bypassed through a link inside the approved folder. (#640)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `get_work_area` now reads work-area points as seconds, as live Premiere 25.2 and 26.5.1 hosts return them. It had divided them by ticks-per-second and reported values near zero. `set_work_area` writes seconds and reads the result back; it fails honestly when a build ignores the write. (#642)
|
|
32
|
+
- `export_sequence` now fails when Premiere rejects the render or writes no file. (#647)
|
|
33
|
+
- UXP source-media timing reads Premiere 26.5's documented synchronous `Media.getStart()` / `getDuration()` before the deprecated Promise-returning `start` / `duration` properties, so `manage_source_media_timing_uxp` can set a start time on 26.5 hosts instead of reporting the command unavailable.
|
|
34
|
+
|
|
35
|
+
### Verification scope
|
|
36
|
+
|
|
37
|
+
- Automated checks validate package behavior. The UXP media timing change and the server-side symlink guard have not been verified on a licensed Premiere host.
|
|
38
|
+
|
|
9
39
|
## [1.18.2] - 2026-09-26
|
|
10
40
|
|
|
11
41
|
### Added
|
package/README.md
CHANGED
|
@@ -110,10 +110,10 @@ 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.
|
|
113
|
+
### Latest release: 1.18.3
|
|
114
114
|
|
|
115
|
-
The published v1.18.
|
|
116
|
-
and
|
|
115
|
+
The published v1.18.3 npm artifact contains **384 core tools**, 382 in its default profile,
|
|
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
|
|
|
119
119
|
### Try a bounded workflow
|
|
@@ -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.
|
|
165
|
+
See the [v1.18.3 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.18.3)
|
|
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.
|
|
213
|
+
> npm i -g premiere-pro-mcp@1.18.3
|
|
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.
|
|
221
|
+
> | Version | `1.18.3` |
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
536
|
+
npx -y premiere-pro-mcp@1.18.3 --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.
|
|
540
|
+
launches `premiere-pro-mcp@1.18.3` 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.
|
|
578
|
+
npx -y premiere-pro-mcp@1.18.3 --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.
|
|
2
|
+
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.4" ExtensionBundleName="MCP for Adobe After Effects">
|
|
3
3
|
<ExtensionList>
|
|
4
|
-
<Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.
|
|
4
|
+
<Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.4"/>
|
|
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.
|
|
2
|
+
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.18.4" ExtensionBundleName="MCP for Adobe Premiere Pro">
|
|
3
3
|
<ExtensionList>
|
|
4
|
-
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.
|
|
5
|
-
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.
|
|
4
|
+
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.4"/>
|
|
5
|
+
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.4"/>
|
|
6
6
|
</ExtensionList>
|
|
7
7
|
<ExecutionEnvironment>
|
|
8
8
|
<HostList>
|
package/cep-plugin/index.html
CHANGED
|
@@ -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.
|
|
84
|
+
<strong id="updateTitle">Version 1.18.4</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>
|
package/cep-plugin/updater.cjs
CHANGED
|
@@ -43,6 +43,14 @@ function __ticksToSeconds(ticks) {
|
|
|
43
43
|
return parseFloat(ticks) / TICKS_PER_SECOND;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Sequence work-area getters return seconds (often as a string) on live 25.x
|
|
47
|
+
// and 26.x hosts. Values above 1e6 can only be ticks, so convert those.
|
|
48
|
+
function __workAreaSeconds(value) {
|
|
49
|
+
var number = parseFloat(value);
|
|
50
|
+
if (!isFinite(number) || number <= -399999) return null;
|
|
51
|
+
return number > 1000000 ? number / TICKS_PER_SECOND : number;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function __secondsToTicks(seconds) {
|
|
47
55
|
return Math.round(parseFloat(seconds) * TICKS_PER_SECOND);
|
|
48
56
|
}
|
|
@@ -2,8 +2,11 @@ import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { WebSocket, WebSocketServer } from "ws";
|
|
5
|
+
import { assertNoSymlinkedPaths, SymlinkPathError } from "../security/path-guard.js";
|
|
5
6
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
6
7
|
const SUPPORTED_PROTOCOLS = new Set([1, 2]);
|
|
8
|
+
/** Must match MAX_COMMAND_BYTES in uxp-plugin/protocol.cjs. */
|
|
9
|
+
export const MAX_UXP_COMMAND_BYTES = 64 * 1024;
|
|
7
10
|
export class UxpBridgeError extends Error {
|
|
8
11
|
code;
|
|
9
12
|
constructor(code, message) {
|
|
@@ -125,25 +128,41 @@ export class UxpWebSocketBridge extends EventEmitter {
|
|
|
125
128
|
if (hello.commands[command]?.supported !== true) {
|
|
126
129
|
throw new UxpBridgeError("UXP_COMMAND_UNSUPPORTED", `Connected Premiere host does not support UXP command '${command}'`);
|
|
127
130
|
}
|
|
131
|
+
// The panel cannot see through links on Premiere 26.5 UXP (#640), so refuse
|
|
132
|
+
// symlinked path segments here, before anything reaches the host.
|
|
133
|
+
try {
|
|
134
|
+
assertNoSymlinkedPaths(args);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error instanceof SymlinkPathError)
|
|
138
|
+
throw new UxpBridgeError(error.code, error.message);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
128
141
|
const minimumTimeoutMs = requestOptions.minimumTimeoutMs ?? 0;
|
|
129
142
|
if (!Number.isInteger(minimumTimeoutMs) || minimumTimeoutMs < 0) {
|
|
130
143
|
throw new Error("UXP minimum request timeout must be a non-negative integer");
|
|
131
144
|
}
|
|
132
145
|
const requestTimeoutMs = Math.max(this.options.requestTimeoutMs, minimumTimeoutMs);
|
|
133
146
|
const requestId = randomUUID();
|
|
147
|
+
const frame = JSON.stringify({
|
|
148
|
+
protocolVersion: hello.protocolVersion,
|
|
149
|
+
type: "command",
|
|
150
|
+
requestId,
|
|
151
|
+
command,
|
|
152
|
+
args,
|
|
153
|
+
});
|
|
154
|
+
// The panel drops frames over 64 KiB before it can read the requestId, so an
|
|
155
|
+
// oversized command would only surface as a timeout. Refuse it here instead.
|
|
156
|
+
if (Buffer.byteLength(frame, "utf8") > MAX_UXP_COMMAND_BYTES) {
|
|
157
|
+
throw new UxpBridgeError("UXP_COMMAND_TOO_LARGE", `UXP command '${command}' is larger than the panel's 64 KiB limit. Send less data (for example a narrower metadata update). No command was sent to Premiere.`);
|
|
158
|
+
}
|
|
134
159
|
return new Promise((resolve, reject) => {
|
|
135
160
|
const timer = setTimeout(() => {
|
|
136
161
|
this.pending.delete(requestId);
|
|
137
162
|
reject(new UxpBridgeError("UXP_TIMEOUT", `UXP command '${command}' timed out`));
|
|
138
163
|
}, requestTimeoutMs);
|
|
139
164
|
this.pending.set(requestId, { command, resolve, reject, timer });
|
|
140
|
-
socket.send(
|
|
141
|
-
protocolVersion: hello.protocolVersion,
|
|
142
|
-
type: "command",
|
|
143
|
-
requestId,
|
|
144
|
-
command,
|
|
145
|
-
args,
|
|
146
|
-
}), (error) => {
|
|
165
|
+
socket.send(frame, (error) => {
|
|
147
166
|
if (!error)
|
|
148
167
|
return;
|
|
149
168
|
const pending = this.pending.get(requestId);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class SymlinkPathError extends Error {
|
|
2
|
+
readonly argumentPath: string;
|
|
3
|
+
readonly code = "UXP_PATH_SYMLINK_REFUSED";
|
|
4
|
+
constructor(argumentPath: string);
|
|
5
|
+
}
|
|
6
|
+
/** Returns the first symlinked segment of an absolute path, or null. */
|
|
7
|
+
export declare function findSymlinkedSegment(value: string): string | null;
|
|
8
|
+
/** Throws SymlinkPathError when any absolute path argument crosses a link. */
|
|
9
|
+
export declare function assertNoSymlinkedPaths(args: unknown): void;
|
|
10
|
+
//# sourceMappingURL=path-guard.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { lstatSync, readlinkSync } from "node:fs";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Server-side symlink confinement for UXP path arguments (issue #640).
|
|
5
|
+
*
|
|
6
|
+
* The UXP panel checks lexical containment inside the approved workspace, but
|
|
7
|
+
* Premiere 26.5 UXP cannot see through links: `Entry.nativePath` reports the
|
|
8
|
+
* link's own path and `lstat` is unimplemented. The MCP server runs on the same
|
|
9
|
+
* machine as Premiere, so it walks every absolute path argument with Node's
|
|
10
|
+
* real filesystem before a command is sent and refuses any symlinked or
|
|
11
|
+
* junctioned segment. Segments that do not exist yet (new output files) end
|
|
12
|
+
* the walk; they cannot redirect anything.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_DEPTH = 8;
|
|
15
|
+
const MAX_STRINGS = 512;
|
|
16
|
+
/** macOS calibration links that always point into /private. */
|
|
17
|
+
const SYSTEM_LINK_TARGETS = {
|
|
18
|
+
"/tmp": ["private/tmp", "/private/tmp"],
|
|
19
|
+
"/var": ["private/var", "/private/var"],
|
|
20
|
+
"/etc": ["private/etc", "/private/etc"],
|
|
21
|
+
};
|
|
22
|
+
export class SymlinkPathError extends Error {
|
|
23
|
+
argumentPath;
|
|
24
|
+
code = "UXP_PATH_SYMLINK_REFUSED";
|
|
25
|
+
constructor(argumentPath) {
|
|
26
|
+
super(`${argumentPath} goes through a symbolic link or junction. Use the real folder inside the approved workspace. No command was sent to Premiere.`);
|
|
27
|
+
this.argumentPath = argumentPath;
|
|
28
|
+
this.name = "SymlinkPathError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function absoluteFlavor(value) {
|
|
32
|
+
if (/^[A-Za-z]:[\\/]/.test(value) || /^\\\\[^\\]/.test(value))
|
|
33
|
+
return win32;
|
|
34
|
+
if (value.startsWith("/") && !value.startsWith("//"))
|
|
35
|
+
return posix;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function isSystemLink(segment) {
|
|
39
|
+
const targets = SYSTEM_LINK_TARGETS[segment];
|
|
40
|
+
if (!targets || process.platform !== "darwin")
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
return targets.includes(readlinkSync(segment));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Returns the first symlinked segment of an absolute path, or null. */
|
|
50
|
+
export function findSymlinkedSegment(value) {
|
|
51
|
+
const flavor = absoluteFlavor(value);
|
|
52
|
+
if (!flavor || value.includes("\0"))
|
|
53
|
+
return null;
|
|
54
|
+
const normalized = flavor.normalize(value);
|
|
55
|
+
const root = flavor.parse(normalized).root;
|
|
56
|
+
const parts = normalized.slice(root.length).split(flavor.sep).filter(Boolean);
|
|
57
|
+
let current = root;
|
|
58
|
+
for (const part of parts) {
|
|
59
|
+
current = flavor.join(current, part);
|
|
60
|
+
let stats;
|
|
61
|
+
try {
|
|
62
|
+
stats = lstatSync(current);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (stats.isSymbolicLink()) {
|
|
68
|
+
if (!isSystemLink(current))
|
|
69
|
+
return current;
|
|
70
|
+
// Keep walking below /tmp, /var and /etc: lstat of later segments
|
|
71
|
+
// resolves through the calibration link and still sees user links.
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!stats.isDirectory())
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function collectStrings(value, path, out, depth) {
|
|
80
|
+
if (out.length >= MAX_STRINGS || depth > MAX_DEPTH)
|
|
81
|
+
return;
|
|
82
|
+
if (typeof value === "string") {
|
|
83
|
+
if (absoluteFlavor(value))
|
|
84
|
+
out.push([path, value]);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
value.forEach((entry, index) => collectStrings(entry, `${path}[${index}]`, out, depth + 1));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (value && typeof value === "object") {
|
|
92
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
93
|
+
collectStrings(entry, path ? `${path}.${key}` : key, out, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Throws SymlinkPathError when any absolute path argument crosses a link. */
|
|
98
|
+
export function assertNoSymlinkedPaths(args) {
|
|
99
|
+
const candidates = [];
|
|
100
|
+
collectStrings(args, "", candidates, 0);
|
|
101
|
+
for (const [argumentPath, value] of candidates) {
|
|
102
|
+
if (findSymlinkedSegment(value))
|
|
103
|
+
throw new SymlinkPathError(argumentPath || "path");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=path-guard.js.map
|
package/dist/tools/advanced.js
CHANGED
|
@@ -649,9 +649,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
649
649
|
var qeTrack = result.trackType === "video"
|
|
650
650
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
651
651
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
652
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
653
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
654
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
655
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
656
|
+
|
|
655
657
|
qeClip.removeEffects();
|
|
656
658
|
return __result({ removed: true, clipName: result.clip.name });
|
|
657
659
|
`);
|
|
@@ -738,9 +740,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
738
740
|
var qeTrack = result.trackType === "video"
|
|
739
741
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
740
742
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
743
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
744
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
745
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
746
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
747
|
+
|
|
744
748
|
qeClip.setFrameBlend(${args.enabled});
|
|
745
749
|
return __result({ frameBlend: ${args.enabled}, clipName: result.clip.name });
|
|
746
750
|
`);
|
|
@@ -775,9 +779,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
775
779
|
var qeTrack = result.trackType === "video"
|
|
776
780
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
777
781
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
782
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
783
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
784
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
785
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
786
|
+
|
|
781
787
|
qeClip.setTimeInterpolationType(${args.interpolation_type});
|
|
782
788
|
var typeNames = ["Frame Sampling", "Frame Blending", "Optical Flow"];
|
|
783
789
|
return __result({
|
|
@@ -817,9 +823,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
817
823
|
var qeTrack = result.trackType === "video"
|
|
818
824
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
819
825
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
826
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
827
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
828
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
829
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
830
|
+
|
|
823
831
|
var oldName = result.clip.name;
|
|
824
832
|
qeClip.setName("${escapeForExtendScript(args.new_name)}");
|
|
825
833
|
return __result({ renamed: true, oldName: oldName, newName: "${escapeForExtendScript(args.new_name)}" });
|
package/dist/tools/clipboard.js
CHANGED
|
@@ -469,17 +469,39 @@ export function getClipboardTools(bridgeOptions) {
|
|
|
469
469
|
|
|
470
470
|
var src = srcResult.clip;
|
|
471
471
|
var tgt = tgtResult.clip;
|
|
472
|
-
var copied =
|
|
472
|
+
var copied = [];
|
|
473
|
+
var unverified = [];
|
|
474
|
+
var failed = [];
|
|
473
475
|
var effectFilter = ${args.effect_name ? `"${escapeForExtendScript(args.effect_name)}"` : "null"};
|
|
474
476
|
var intrinsic = ["Motion", "Opacity", "Time Remapping", "Volume", "Channel Volume", "Panner"];
|
|
475
477
|
|
|
476
478
|
// Use QE to copy effects by name
|
|
477
479
|
var qeSeq = qe.project.getActiveSequence();
|
|
480
|
+
if (!qeSeq) return __error("No active sequence (QE); nothing was changed.");
|
|
478
481
|
var tgtTrackType = tgtResult.trackType;
|
|
479
|
-
var tgtTrack = tgtTrackType === "video"
|
|
480
|
-
? qeSeq.getVideoTrackAt(tgtResult.trackIndex)
|
|
482
|
+
var tgtTrack = tgtTrackType === "video"
|
|
483
|
+
? qeSeq.getVideoTrackAt(tgtResult.trackIndex)
|
|
481
484
|
: qeSeq.getAudioTrackAt(tgtResult.trackIndex);
|
|
482
|
-
|
|
485
|
+
if (!tgtTrack) return __error("QE target track not found; nothing was changed.");
|
|
486
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
487
|
+
var qeTgtClip = __findQeClipByDomClip(tgtTrack, tgt);
|
|
488
|
+
if (!qeTgtClip) return __error("Could not match the QE clip for target " + tgt.name + " by timeline start; nothing was changed.");
|
|
489
|
+
|
|
490
|
+
function countTargetComponents(wanted) {
|
|
491
|
+
var n = 0;
|
|
492
|
+
for (var ci = 0; ci < tgt.components.numItems; ci++) {
|
|
493
|
+
if (tgt.components[ci].displayName === wanted) n++;
|
|
494
|
+
}
|
|
495
|
+
return n;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function listNames(entries) {
|
|
499
|
+
var out = [];
|
|
500
|
+
for (var li = 0; li < entries.length; li++) {
|
|
501
|
+
out.push(typeof entries[li] === "string" ? entries[li] : entries[li].effect + " (" + entries[li].reason + ")");
|
|
502
|
+
}
|
|
503
|
+
return out.join(", ");
|
|
504
|
+
}
|
|
483
505
|
|
|
484
506
|
for (var i = 0; i < src.components.numItems; i++) {
|
|
485
507
|
var comp = src.components[i];
|
|
@@ -494,23 +516,61 @@ export function getClipboardTools(bridgeOptions) {
|
|
|
494
516
|
if (skip) continue;
|
|
495
517
|
}
|
|
496
518
|
|
|
497
|
-
// Apply effect via QE
|
|
519
|
+
// Apply effect via QE, then read the target component list back.
|
|
520
|
+
var beforeCount = null;
|
|
521
|
+
try { beforeCount = countTargetComponents(name); } catch (beforeErr) {}
|
|
522
|
+
var qeEffect = null;
|
|
498
523
|
try {
|
|
499
|
-
|
|
524
|
+
qeEffect = tgtTrackType === "video"
|
|
500
525
|
? qe.project.getVideoEffectByName(name)
|
|
501
526
|
: qe.project.getAudioEffectByName(name);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
527
|
+
} catch (lookupErr) {
|
|
528
|
+
failed.push({ effect: name, reason: "QE effect lookup failed: " + lookupErr.toString() });
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (!qeEffect) {
|
|
532
|
+
failed.push({ effect: name, reason: "QE did not resolve a " + tgtTrackType + " effect with this name" });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
if (tgtTrackType === "video") {
|
|
537
|
+
qeTgtClip.addVideoEffect(qeEffect);
|
|
538
|
+
} else {
|
|
539
|
+
qeTgtClip.addAudioEffect(qeEffect);
|
|
509
540
|
}
|
|
510
|
-
} catch(
|
|
541
|
+
} catch (addErr) {
|
|
542
|
+
failed.push({ effect: name, reason: "Premiere rejected the effect: " + addErr.toString() });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
var afterCount = null;
|
|
546
|
+
try { afterCount = countTargetComponents(name); } catch (afterErr) {}
|
|
547
|
+
if (beforeCount !== null && afterCount !== null && afterCount > beforeCount) {
|
|
548
|
+
copied.push(name);
|
|
549
|
+
} else {
|
|
550
|
+
unverified.push({ effect: name, reason: "Premiere accepted the call but the target component list did not show a new " + name + " component" });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (failed.length > 0) {
|
|
555
|
+
var partial = copied.length + unverified.length > 0;
|
|
556
|
+
return __error((partial ? "copy_effects_between_clips was only partially applied" : "No effects were copied") +
|
|
557
|
+
" to " + tgt.name + ". Verified: [" + listNames(copied) + "]; committed_unverified: [" + listNames(unverified) +
|
|
558
|
+
"]; failed: [" + listNames(failed) + "]. Check Effect Controls before retrying.");
|
|
511
559
|
}
|
|
512
560
|
|
|
513
|
-
|
|
561
|
+
var status = copied.length + unverified.length === 0
|
|
562
|
+
? "unchanged"
|
|
563
|
+
: (unverified.length > 0 ? "committed_unverified" : "verified");
|
|
564
|
+
return __result({
|
|
565
|
+
status: status,
|
|
566
|
+
verified: status === "verified",
|
|
567
|
+
copiedEffects: copied.length,
|
|
568
|
+
copied: copied,
|
|
569
|
+
committedUnverified: unverified,
|
|
570
|
+
failed: failed,
|
|
571
|
+
source: src.name,
|
|
572
|
+
target: tgt.name
|
|
573
|
+
});
|
|
514
574
|
`);
|
|
515
575
|
return sendCommand(script, bridgeOptions);
|
|
516
576
|
},
|
package/dist/tools/effects.js
CHANGED
|
@@ -34,10 +34,11 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
34
34
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
35
35
|
|
|
36
36
|
if (!qeTrack) return __error("QE track not found");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
|
|
38
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
39
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
40
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
41
|
+
|
|
41
42
|
// Premiere 26 can expose direct by-name lookup while returning an
|
|
42
43
|
// empty QE catalog. Probe the requested name before consulting the
|
|
43
44
|
// catalog so a known installed effect remains usable.
|
|
@@ -46,7 +47,7 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
46
47
|
var lookupSource = qeEffect ? "qe.byName" : "qe.catalog";
|
|
47
48
|
if (!qeEffect) {
|
|
48
49
|
var effectCatalog = __getQeEffectCatalog("video");
|
|
49
|
-
if (!effectCatalog.ok) return __error(effectCatalog.error + " Direct QE lookup for
|
|
50
|
+
if (!effectCatalog.ok) return __error(effectCatalog.error + " Direct QE lookup for \\"" + effectName + "\\" also did not resolve an effect.");
|
|
50
51
|
var effects = effectCatalog.effects;
|
|
51
52
|
for (var i = 0; i < effects.numItems; i++) {
|
|
52
53
|
if (effects[i].name === effectName) {
|
|
@@ -90,16 +91,17 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
90
91
|
var effectName = "${escapeForExtendScript(args.effect_name)}";
|
|
91
92
|
var qeTrack = qeSeq.getAudioTrackAt(result.trackIndex);
|
|
92
93
|
if (!qeTrack) return __error("QE audio track not found");
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
|
|
95
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
96
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
97
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
96
98
|
|
|
97
99
|
var qeEffect = null;
|
|
98
100
|
try { qeEffect = qe.project.getAudioEffectByName(effectName); } catch (byNameError) {}
|
|
99
101
|
var lookupSource = qeEffect ? "qe.byName" : "qe.catalog";
|
|
100
102
|
if (!qeEffect) {
|
|
101
103
|
var effectCatalog = __getQeEffectCatalog("audio");
|
|
102
|
-
if (!effectCatalog.ok) return __error(effectCatalog.error + " Direct QE lookup for
|
|
104
|
+
if (!effectCatalog.ok) return __error(effectCatalog.error + " Direct QE lookup for \\"" + effectName + "\\" also did not resolve an effect.");
|
|
103
105
|
var effects = effectCatalog.effects;
|
|
104
106
|
for (var i = 0; i < effects.numItems; i++) {
|
|
105
107
|
if (effects[i].name === effectName) {
|
|
@@ -306,7 +308,9 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
306
308
|
|
|
307
309
|
if (!hasLumetri) {
|
|
308
310
|
var qeTrack = qeSeq.getVideoTrackAt(result.trackIndex);
|
|
309
|
-
|
|
311
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
312
|
+
var qeClip = __findQeClipByDomClip(qeTrack, clip);
|
|
313
|
+
if (!qeClip) return __error("Could not match the QE clip for " + clip.name + " by timeline start; nothing was changed.");
|
|
310
314
|
var effectCatalog = __getQeEffectCatalog("video");
|
|
311
315
|
if (!effectCatalog.ok) return __error(effectCatalog.error);
|
|
312
316
|
var effects = effectCatalog.effects;
|
|
@@ -317,7 +321,7 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
317
321
|
}
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
|
-
|
|
324
|
+
|
|
321
325
|
// A single unsettable property must not abort the script with "Invalid
|
|
322
326
|
// parameter" and lose every other change, so each write is guarded.
|
|
323
327
|
// The taken map is a separate set of flags rather than a truthiness check on
|
|
@@ -379,8 +383,11 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
379
383
|
|
|
380
384
|
if (!lumetriComp) {
|
|
381
385
|
var qeSeq = qe.project.getActiveSequence();
|
|
386
|
+
if (!qeSeq) return __error("No active sequence (QE); nothing was changed.");
|
|
382
387
|
var qeTrack = qeSeq.getVideoTrackAt(result.trackIndex);
|
|
383
|
-
|
|
388
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
389
|
+
var qeClip = __findQeClipByDomClip(qeTrack, clip);
|
|
390
|
+
if (!qeClip) return __error("Could not match the QE clip for " + clip.name + " by timeline start; nothing was changed.");
|
|
384
391
|
var effectCatalog = __getQeEffectCatalog("video");
|
|
385
392
|
if (!effectCatalog.ok) return __error(effectCatalog.error);
|
|
386
393
|
var effects = effectCatalog.effects;
|
|
@@ -535,9 +542,10 @@ export function getEffectsTools(bridgeOptions) {
|
|
|
535
542
|
: null;
|
|
536
543
|
if (!qeTrack) return __error("Warp Stabilizer can only be applied to video clips");
|
|
537
544
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
545
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
546
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
547
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
548
|
+
|
|
541
549
|
// Find and apply Warp Stabilizer
|
|
542
550
|
var effectCatalog = __getQeEffectCatalog("video");
|
|
543
551
|
if (!effectCatalog.ok) return __error(effectCatalog.error);
|
package/dist/tools/export.js
CHANGED
|
@@ -647,8 +647,13 @@ export function getExportTools(bridgeOptions) {
|
|
|
647
647
|
presetPath,
|
|
648
648
|
${args.work_area_only ? "app.encoder.ENCODE_WORKAREA" : "app.encoder.ENCODE_ENTIRE"}
|
|
649
649
|
);
|
|
650
|
+
// Premiere documents a boolean; false means the render was rejected.
|
|
651
|
+
// Some hosts throw instead; buildToolScript already maps that to __error.
|
|
652
|
+
if (exportResult === false) return __error("Premiere rejected the sequence export.");
|
|
653
|
+
var outputFile = new File(outputPath);
|
|
654
|
+
if (!outputFile.exists) return __error("Premiere did not write the requested export file.");
|
|
650
655
|
|
|
651
|
-
return __result({ exported: true, outputPath: outputPath, presetUsed: presetPath });
|
|
656
|
+
return __result({ exported: true, verified: true, outputPath: outputPath, presetUsed: presetPath });
|
|
652
657
|
`);
|
|
653
658
|
return sendCommand(script, { ...bridgeOptions, timeoutMs: 120000 }); // 2 min timeout for exports
|
|
654
659
|
},
|
package/dist/tools/playhead.js
CHANGED
|
@@ -65,10 +65,21 @@ export function getPlayheadTools(bridgeOptions) {
|
|
|
65
65
|
var seq = app.project.activeSequence;
|
|
66
66
|
if (!seq) return __error("No active sequence");
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// Live hosts (25.2, 26.5.1) read and write work-area points in seconds,
|
|
69
|
+
// not ticks. Write seconds, then read back: some builds ignore the write.
|
|
70
|
+
var requestedIn = ${Number(args.in_seconds)};
|
|
71
|
+
var requestedOut = ${Number(args.out_seconds)};
|
|
72
|
+
if (!(requestedOut > requestedIn)) return __error("out_seconds must be greater than in_seconds.");
|
|
73
|
+
seq.setWorkAreaInPoint(requestedIn);
|
|
74
|
+
seq.setWorkAreaOutPoint(requestedOut);
|
|
75
|
+
var observedIn = __workAreaSeconds(seq.getWorkAreaInPoint());
|
|
76
|
+
var observedOut = __workAreaSeconds(seq.getWorkAreaOutPoint());
|
|
77
|
+
var frameSeconds = seq.timebase ? __ticksToSeconds(seq.timebase) : 1 / 24;
|
|
78
|
+
if (observedIn === null || observedOut === null ||
|
|
79
|
+
Math.abs(observedIn - requestedIn) > frameSeconds || Math.abs(observedOut - requestedOut) > frameSeconds) {
|
|
80
|
+
return __error("Premiere did not apply the work area (read back " + observedIn + " to " + observedOut + " s). Use set_sequence_in_out_points to mark an export range instead.");
|
|
81
|
+
}
|
|
82
|
+
return __result({ workAreaIn: observedIn, workAreaOut: observedOut, verified: true });
|
|
72
83
|
`);
|
|
73
84
|
return sendCommand(script, bridgeOptions);
|
|
74
85
|
},
|
|
@@ -83,8 +94,10 @@ export function getPlayheadTools(bridgeOptions) {
|
|
|
83
94
|
var inPoint = seq.getWorkAreaInPoint();
|
|
84
95
|
var outPoint = seq.getWorkAreaOutPoint();
|
|
85
96
|
return __result({
|
|
86
|
-
inSeconds:
|
|
87
|
-
outSeconds:
|
|
97
|
+
inSeconds: __workAreaSeconds(inPoint),
|
|
98
|
+
outSeconds: __workAreaSeconds(outPoint),
|
|
99
|
+
rawIn: String(inPoint),
|
|
100
|
+
rawOut: String(outPoint)
|
|
88
101
|
});
|
|
89
102
|
`);
|
|
90
103
|
return sendCommand(script, bridgeOptions);
|
|
@@ -1129,16 +1129,27 @@ export function getTrackTargetingTools(bridgeOptions) {
|
|
|
1129
1129
|
var num = ${startNum};
|
|
1130
1130
|
var pattern = "${escapeForExtendScript(args.pattern)}";
|
|
1131
1131
|
|
|
1132
|
+
// QE track items include gaps, so the DOM clip index is not a QE
|
|
1133
|
+
// index. Match every target by timeline start before renaming any.
|
|
1134
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
1135
|
+
var targets = [];
|
|
1132
1136
|
for (var c = 0; c < track.clips.numItems; c++) {
|
|
1133
1137
|
var clip = track.clips[c];
|
|
1134
1138
|
${args.selected_only ? `if (!clip.isSelected()) continue;` : ""}
|
|
1139
|
+
var matchedQeClip = __findQeClipByDomClip(qeTrack, clip);
|
|
1140
|
+
if (!matchedQeClip) return __error("Could not match the QE clip for " + clip.name + " by timeline start; nothing was changed.");
|
|
1141
|
+
targets.push({ clip: clip, qeClip: matchedQeClip });
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
for (var t = 0; t < targets.length; t++) {
|
|
1145
|
+
var clip = targets[t].clip;
|
|
1135
1146
|
|
|
1136
1147
|
var sequenceNumber = "" + num;
|
|
1137
1148
|
var paddedSequenceNumber = sequenceNumber;
|
|
1138
1149
|
while (paddedSequenceNumber.length < 2) paddedSequenceNumber = "0" + paddedSequenceNumber;
|
|
1139
1150
|
var newName = pattern.split("{n}").join(sequenceNumber).split("##").join(paddedSequenceNumber).split("{name}").join(clip.name);
|
|
1140
1151
|
try {
|
|
1141
|
-
var qeClip =
|
|
1152
|
+
var qeClip = targets[t].qeClip;
|
|
1142
1153
|
qeClip.setName(newName);
|
|
1143
1154
|
renamed++;
|
|
1144
1155
|
} catch(e) {}
|
|
@@ -1363,8 +1374,16 @@ export function getTrackTargetingTools(bridgeOptions) {
|
|
|
1363
1374
|
: `qeSeq.getAudioTrackAt(${args.track_index})`};
|
|
1364
1375
|
if (!qeTrack) return __error("Track not found");
|
|
1365
1376
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1377
|
+
// clip_index addresses the DOM clip list; QE track items also include
|
|
1378
|
+
// gaps, so resolve the DOM clip and match its QE item by start time.
|
|
1379
|
+
var seq = app.project.activeSequence;
|
|
1380
|
+
if (!seq) return __error("No active sequence");
|
|
1381
|
+
var domTracks = ${args.track_type === "video" ? "seq.videoTracks" : "seq.audioTracks"};
|
|
1382
|
+
if (${args.track_index} >= domTracks.numTracks) return __error("Track not found");
|
|
1383
|
+
var domClips = domTracks[${args.track_index}].clips;
|
|
1384
|
+
if (${args.clip_index} >= domClips.numItems) return __error("Clip not found at index ${args.clip_index}");
|
|
1385
|
+
var qeClip = __findQeClipByDomClip(qeTrack, domClips[${args.clip_index}]);
|
|
1386
|
+
if (!qeClip) return __error("Could not match the QE clip for clip index ${args.clip_index} by timeline start");
|
|
1368
1387
|
|
|
1369
1388
|
var info = { trackType: "${args.track_type}", trackIndex: ${args.track_index}, clipIndex: ${args.clip_index} };
|
|
1370
1389
|
|
package/dist/tools/uxp.js
CHANGED
|
@@ -570,7 +570,7 @@ export function getUxpTools(bridge) {
|
|
|
570
570
|
},
|
|
571
571
|
},
|
|
572
572
|
import_transcript_uxp: {
|
|
573
|
-
description: "
|
|
573
|
+
description: "Import native transcript JSON onto one untranscribed source media clip through documented Premiere 26.3+ UXP APIs. Check has_transcript_uxp first and omit expected_transcript_revision (or pass null). Importing over an existing transcript is refused (UXP_TRANSCRIPT_OVERWRITE_REFUSED): on Premiere 26.5.1 a failed import cleared an existing transcript that could not be restored. This destructive import requires explicit confirmation and an operation_id, serializes competing imports for the same project item, runs one undoable transaction, and reports exact bounded export SHA-256 readback rather than claiming a licensed-host result.",
|
|
574
574
|
parameters: {
|
|
575
575
|
type: "object",
|
|
576
576
|
additionalProperties: false,
|
|
@@ -458,7 +458,7 @@ authenticated and the connected host advertises the required command capabilitie
|
|
|
458
458
|
| `get_uxp_workspace_access` | Connected UXP | Single operation | Report whether the Premiere panel has an operator-approved persistent workspace folder. The native path and persistent token are never returned. |
|
|
459
459
|
| `has_transcript_uxp` | Connected UXP | Single operation | Check whether a source media clip has a transcript, preferring Premiere 26.3's documented native API when available. |
|
|
460
460
|
| `import_project_media_uxp` | Connected UXP | `files`, `sequences`, `ae_comps`, `all_ae_comps` | Import workspace-contained media files, sequences, or After Effects compositions through documented Project APIs with post-state evidence. |
|
|
461
|
-
| `import_transcript_uxp` | Connected UXP | Single operation |
|
|
461
|
+
| `import_transcript_uxp` | Connected UXP | Single operation | Import native transcript JSON onto one untranscribed source media clip through documented Premiere 26.3+ UXP APIs. Check has_transcript_uxp first and omit expected_transcript_revision (or pass null). Importing over an existing transcript is refused (UXP_TRANSCRIPT_OVERWRITE_REFUSED): on Premiere 26.5.1 a failed import cleared an existing transcript that could not be restored. This destructive import requires explicit confirmation and an operation_id, serializes competing imports for the same project item, runs one undoable transaction, and reports exact bounded export SHA-256 readback rather than claiming a licensed-host result. |
|
|
462
462
|
| `inspect_caption_tracks_uxp` | Connected UXP | Single operation | Inventory native caption tracks on the active sequence through documented Premiere UXP APIs. Returns track identity, name, mute state, and item count only; it does not inspect cue text, timing, rendered appearance, or caption correctness. |
|
|
463
463
|
| `inspect_effect_parameter_catalog_uxp` | Connected UXP | `media_type`: `video`, `audio` | Inspect the bounded native descriptor catalog for one effect component on one active-sequence audio or video clip. The returned parameter index and display name can be used with existing parameter automation. It returns only animation capability/state, never raw parameter values (including Color or PointF), media paths, rendered pixels, or playback results. The bridge reads the complete catalog twice and rejects a changed project, active sequence, component identity, or parameter descriptor set. |
|
|
464
464
|
| `inspect_frame_alignment_uxp` | Connected UXP | `align`, `frame` | Use Premiere's documented native TickTime and FrameRate APIs to either align one bounded requested time down or to the nearest frame boundary, or construct the exact TickTime for one frame count. This is read-only, uses only caller-owned inputs, and returns native seconds and tick-string readback; it does not inspect a sequence, infer its frame rate, change Premiere, or prove a licensed host. |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premiere-pro-mcp",
|
|
3
3
|
"mcpName": "io.github.leancoderkavy/premiere-pro",
|
|
4
|
-
"version": "1.18.
|
|
4
|
+
"version": "1.18.4",
|
|
5
5
|
"description": "MCP server for Adobe Premiere Pro with 384 core AI video editing tools, a guarded After Effects MOGRT studio, and a capability-aware UXP bridge for supported Premiere workflows.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "MCP for Adobe Premiere Pro",
|
|
11
11
|
"mcpName": "io.github.leancoderkavy/premiere-pro",
|
|
12
12
|
"npmPackage": "premiere-pro-mcp",
|
|
13
|
-
"version": "1.18.
|
|
13
|
+
"version": "1.18.4",
|
|
14
14
|
"repository": "https://github.com/leancoderkavy/premiere-pro-mcp",
|
|
15
15
|
"homepage": "https://premiere-pro-mcp.com/",
|
|
16
16
|
"license": "MIT",
|
package/uxp-plugin/manifest.json
CHANGED
|
@@ -489,7 +489,7 @@
|
|
|
489
489
|
assertOnlyKeys(args, ["path", "confirmExternalWrite", "confirmOverwrite", "operationId"]);
|
|
490
490
|
requireConfirmation(args.confirmExternalWrite, "confirmExternalWrite", "Creating a project writes a new file");
|
|
491
491
|
const path = await allowedWorkspacePath(args.path, "path");
|
|
492
|
-
rejectExistingProject(path, args.confirmOverwrite);
|
|
492
|
+
await rejectExistingProject(path, args.confirmOverwrite);
|
|
493
493
|
const project = await ppro.Project.createProject(path);
|
|
494
494
|
assertProjectPath(project, path, "created project");
|
|
495
495
|
return projectMutationReceipt("created", project, path);
|
|
@@ -520,7 +520,7 @@
|
|
|
520
520
|
const project = await targetProject(args.projectId);
|
|
521
521
|
if (args.expectedPath != null) assertProjectPath(project, requiredPath(args.expectedPath, "expectedPath"), "target project");
|
|
522
522
|
const path = await allowedWorkspacePath(args.path, "path");
|
|
523
|
-
rejectExistingProject(path, args.confirmOverwrite);
|
|
523
|
+
await rejectExistingProject(path, args.confirmOverwrite);
|
|
524
524
|
if (typeof project.saveAs !== "function" || !await project.saveAs(path)) {
|
|
525
525
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm Save As");
|
|
526
526
|
}
|
|
@@ -543,7 +543,7 @@
|
|
|
543
543
|
if (samePath(path, sourcePath) || paths.some(function (item) { return samePath(item, path); })) {
|
|
544
544
|
throw commandError("UXP_INVALID_ARGUMENT", "Branch paths must be distinct from the source and each other");
|
|
545
545
|
}
|
|
546
|
-
rejectExistingProject(path, args.confirmOverwrite);
|
|
546
|
+
await rejectExistingProject(path, args.confirmOverwrite);
|
|
547
547
|
paths.push(path);
|
|
548
548
|
}
|
|
549
549
|
const branches = [];
|
|
@@ -1196,9 +1196,17 @@
|
|
|
1196
1196
|
return { projectItemId, startSeconds: start, durationSeconds: duration };
|
|
1197
1197
|
}
|
|
1198
1198
|
|
|
1199
|
+
// Premiere 26.5 documents synchronous Media.getStart()/getDuration() and
|
|
1200
|
+
// deprecates the start/duration properties, which return Promise<TickTime>.
|
|
1201
|
+
function mediaTimingGetter(media, propertyName) {
|
|
1202
|
+
const getterName = propertyName === "start" ? "getStart" : "getDuration";
|
|
1203
|
+
return media && typeof media[getterName] === "function" ? getterName : null;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1199
1206
|
function synchronousMediaTimingValue(media, propertyName) {
|
|
1200
1207
|
try {
|
|
1201
|
-
const
|
|
1208
|
+
const getterName = mediaTimingGetter(media, propertyName);
|
|
1209
|
+
const value = getterName ? media[getterName]() : media[propertyName];
|
|
1202
1210
|
if (value && typeof value.then === "function") return null;
|
|
1203
1211
|
return boundedMediaTimingSeconds(value);
|
|
1204
1212
|
} catch (_) { return null; }
|
|
@@ -1332,6 +1340,13 @@
|
|
|
1332
1340
|
}
|
|
1333
1341
|
|
|
1334
1342
|
async function mediaTimingValue(media, propertyName) {
|
|
1343
|
+
const getterName = mediaTimingGetter(media, propertyName);
|
|
1344
|
+
if (getterName) {
|
|
1345
|
+
try {
|
|
1346
|
+
const seconds = boundedMediaTimingSeconds(await media[getterName]());
|
|
1347
|
+
if (seconds != null) return { accessor: getterName + "()", seconds };
|
|
1348
|
+
} catch (_) { /* fall back to the deprecated property */ }
|
|
1349
|
+
}
|
|
1335
1350
|
try {
|
|
1336
1351
|
return { accessor: propertyName, seconds: boundedMediaTimingSeconds(await media[propertyName]) };
|
|
1337
1352
|
} catch (_) {
|
|
@@ -1719,9 +1734,32 @@
|
|
|
1719
1734
|
return deps.workspace.assertPathAllowed(path, { label, kind: "file" });
|
|
1720
1735
|
}
|
|
1721
1736
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1737
|
+
// Premiere 26.5.1's Project.isProject(path) is true for any *.prproj path,
|
|
1738
|
+
// even a missing one (#642), so check the file itself when UXP storage can.
|
|
1739
|
+
async function destinationFileExists(path) {
|
|
1740
|
+
if (typeof deps.fileExists === "function") return !!(await deps.fileExists(path));
|
|
1741
|
+
let fs = null;
|
|
1742
|
+
try {
|
|
1743
|
+
const uxp = require("uxp");
|
|
1744
|
+
fs = uxp && uxp.storage && uxp.storage.localFileSystem;
|
|
1745
|
+
} catch (_) { return null; }
|
|
1746
|
+
if (!fs || typeof fs.getEntryWithUrl !== "function") return null;
|
|
1747
|
+
const normalized = String(path).replace(/\\/g, "/");
|
|
1748
|
+
const url = /^[A-Za-z]:\//.test(normalized) ? "file:/" + normalized : "file:" + normalized;
|
|
1749
|
+
try {
|
|
1750
|
+
const entry = await fs.getEntryWithUrl(url);
|
|
1751
|
+
return !!entry;
|
|
1752
|
+
} catch (_) {
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
async function rejectExistingProject(path, confirmation) {
|
|
1758
|
+
if (confirmation === true) return;
|
|
1759
|
+
const exists = await destinationFileExists(path);
|
|
1760
|
+
const existing = exists === null ? ppro.Project.isProject(path) : exists;
|
|
1761
|
+
if (existing) {
|
|
1762
|
+
throw commandError("UXP_CONFIRMATION_REQUIRED", "confirmOverwrite is required when the destination file already exists");
|
|
1725
1763
|
}
|
|
1726
1764
|
}
|
|
1727
1765
|
|
|
@@ -185,6 +185,12 @@
|
|
|
185
185
|
return serialize(input.expectedProjectGuid + ":" + input.projectItemId, async function () {
|
|
186
186
|
const beforeTarget = await resolveTarget(input), before = await transcriptSnapshot(beforeTarget.clip);
|
|
187
187
|
requireExpectedSnapshot(before, input.expectedTranscriptRevision);
|
|
188
|
+
// Overwriting cannot be undone reliably: on Premiere 26.5.1 a live import
|
|
189
|
+
// cleared an existing transcript and it could not be restored (#642).
|
|
190
|
+
// Only import onto clips with no transcript until a verified restore exists.
|
|
191
|
+
if (before.hasTranscript) {
|
|
192
|
+
throw commandError("UXP_TRANSCRIPT_OVERWRITE_REFUSED", "This clip already has a transcript. Importing over it is disabled because a failed import can clear the existing transcript with no restore. Nothing was changed.");
|
|
193
|
+
}
|
|
188
194
|
|
|
189
195
|
// Resolve and snapshot a second time immediately before action creation.
|
|
190
196
|
// The per-owner queue prevents a different operation ID in this panel
|
package/uxp-plugin/workflows.cjs
CHANGED
|
@@ -1232,6 +1232,11 @@
|
|
|
1232
1232
|
assertCommitted(committed, "metadata update");
|
|
1233
1233
|
const after = await metadataSnapshot(clip);
|
|
1234
1234
|
const verified = (!hasProject || after.projectMetadata === projectMetadata) && (!hasXmp || after.xmpMetadata === xmpMetadata);
|
|
1235
|
+
// Premiere 26.5.1 can commit the transaction yet leave the packet untouched
|
|
1236
|
+
// (#642). A readback identical to the snapshot is a failure, not success.
|
|
1237
|
+
if (!verified && after.projectMetadata === before.projectMetadata && after.xmpMetadata === before.xmpMetadata) {
|
|
1238
|
+
throw commandError("UXP_METADATA_NOT_APPLIED", "Premiere committed the metadata transaction but the metadata read back unchanged. The update was not applied.");
|
|
1239
|
+
}
|
|
1235
1240
|
return mutationResult(verified, { updated: true, updatedFields, metadata: after }, "metadata_readback", "Update clip metadata");
|
|
1236
1241
|
}
|
|
1237
1242
|
|
|
@@ -1464,6 +1469,9 @@
|
|
|
1464
1469
|
let readback = "";
|
|
1465
1470
|
try { readback = propertyString(new xmp.XMPMeta(afterXml || "").getProperty(namespace, name)); } catch (_) {}
|
|
1466
1471
|
const verified = readback === value;
|
|
1472
|
+
if (!verified && readback === current) {
|
|
1473
|
+
throw commandError("UXP_METADATA_NOT_APPLIED", "Premiere committed the metadata field transaction but " + name + " still reads back its previous value. The update was not applied.");
|
|
1474
|
+
}
|
|
1467
1475
|
return mutationResult(verified, {
|
|
1468
1476
|
updated: true, packet, namespace, name, value: readback, requestedValue: value
|
|
1469
1477
|
}, "metadata_field_readback", "Update clip metadata field");
|